use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;

use super::stylesheet::Stylesheet;
use crate::cli::parameters::BuildArgs;
use crate::command::build::render::SiteRenderer;
use crate::config::Config;
use crate::error::CustomError;
use crate::intermediate::get_org_files;
use crate::intermediate::BlogPost;
use crate::intermediate::IPage;
use crate::intermediate::IntermediateContext;
use crate::intermediate::PageInput;
use crate::intermediate::Registry;
use crate::walk_fs::walk_fs;
use crate::walk_fs::WalkAction;
use crate::walk_fs::WalkFsFilterResult;
use include_dir::include_dir;
use include_dir::Dir;
use tokio::fs::DirEntry;

static DEFAULT_STYLESHEETS: Dir =
    include_dir!("$CARGO_MANIFEST_DIR/default_environment/stylesheet");

pub(crate) async fn build_site(args: BuildArgs) -> Result<(), CustomError> {
    let config = Config::load_from_file(args.config).await?;
    let blog_posts = load_blog_posts(&config).await?;
    let stylesheets = load_stylesheets().await?;
    let pages = load_pages(&config).await?;
    let renderer = SiteRenderer::new(
        get_output_directory(&config).await?,
        blog_posts,
        stylesheets,
        pages,
    );
    renderer.render_blog_posts(&config).await?;
    renderer.render_blog_stream(&config).await?;
    renderer.render_pages(&config).await?;
    renderer.render_stylesheets().await?;
    renderer.copy_static_files(&config).await?;

    Ok(())
}

/// Delete everything inside the output directory and return the path to that directory.
async fn get_output_directory(config: &Config) -> Result<PathBuf, CustomError> {
    let output_directory = config.get_output_directory();
    if !output_directory.exists() {
        tokio::fs::create_dir(&output_directory).await?;
    } else {
        let mut existing_entries = tokio::fs::read_dir(&output_directory).await?;
        while let Some(entry) = existing_entries.next_entry().await? {
            let file_type = entry.file_type().await?;
            if file_type.is_dir() {
                tokio::fs::remove_dir_all(entry.path()).await?;
            } else {
                tokio::fs::remove_file(entry.path()).await?;
            }
        }
    }
    Ok(output_directory)
}

async fn filter_to_highest_folders_containing_org_files(entry: &DirEntry) -> WalkFsFilterResult {
    let file_type = entry.file_type().await?;
    if !file_type.is_dir() {
        return Ok(WalkAction::Halt);
    }
    let mut entries = tokio::fs::read_dir(entry.path()).await?;
    while let Some(entry) = entries.next_entry().await? {
        let entry_type = entry.file_type().await?;
        if !entry_type.is_file() {
            continue;
        }
        match entry.path().extension().and_then(OsStr::to_str) {
            Some(ext) if ext.eq_ignore_ascii_case("org") => {
                return Ok(WalkAction::HaltAndCapture);
            }
            _ => {}
        }
    }
    Ok(WalkAction::Recurse)
}

async fn get_post_directories(config: &Config) -> Result<Vec<PathBuf>, CustomError> {
    if !config.get_posts_directory().exists() {
        return Ok(Vec::new());
    }

    let top_level_org_folders = walk_fs(
        config.get_posts_directory(),
        filter_to_highest_folders_containing_org_files,
    )
    .await?;
    Ok(top_level_org_folders
        .into_iter()
        .map(|entry| entry.path())
        .collect())
}

async fn load_blog_posts(config: &Config) -> Result<Vec<BlogPost>, CustomError> {
    let root_directory = config.get_root_directory().to_owned();
    let posts_directory = config.get_posts_directory();
    let post_directories = get_post_directories(config).await?;
    let load_jobs = post_directories.into_iter().map(|path| {
        tokio::spawn(BlogPost::load_blog_post(
            root_directory.clone(),
            posts_directory.clone(),
            path,
        ))
    });
    let mut blog_posts = Vec::new();
    for job in load_jobs {
        blog_posts.push(job.await??);
    }
    Ok(blog_posts)
}

async fn load_pages(config: &Config) -> Result<Vec<IPage>, CustomError> {
    let pages_source = config
        .get_root_directory()
        .join(config.get_relative_path_to_pages());
    if !pages_source.exists() {
        return Ok(Vec::new());
    }
    let page_files = get_org_files(&pages_source).await?;
    let org_files = {
        let mut ret = Vec::new();
        for page in page_files {
            ret.push(page.await??);
        }
        ret
    };
    let parsed_org_files = {
        let mut ret = Vec::new();
        for (path, contents) in org_files.iter() {
            let parsed = organic::parser::parse_file(contents.as_str(), Some(path))
                .map_err(|_| CustomError::Static("Failed to parse org-mode document."))?;
            ret.push((path, contents, parsed));
        }
        ret
    };

    let pages = {
        let mut ret = Vec::new();
        for (real_path, _contents, parsed_document) in parsed_org_files.iter() {
            let mut registry = Registry::new();

            // Assign IDs to the targets
            organic::types::AstNode::from(parsed_document)
                .iter_all_ast_nodes()
                .for_each(|node| {
                    if let organic::types::AstNode::Target(target) = node {
                        registry.get_target(target.value);
                    }
                });

            let registry = Arc::new(Mutex::new(registry));
            let intermediate_context = IntermediateContext::new(registry)?;
            let relative_to_pages_dir_path = real_path.strip_prefix(&pages_source)?;
            ret.push(
                IPage::new(
                    intermediate_context,
                    PageInput::new(relative_to_pages_dir_path, real_path, parsed_document),
                )
                .await?,
            );
        }
        ret
    };

    Ok(pages)
}

async fn load_stylesheets() -> Result<Vec<Stylesheet>, CustomError> {
    let sources: Vec<_> = DEFAULT_STYLESHEETS
        .files()
        .filter(|f| f.path().extension() == Some(OsStr::new("css")))
        .collect();
    let mut ret = Vec::with_capacity(sources.len());
    for entry in sources {
        let path = entry.path().to_path_buf();
        let contents = String::from_utf8(entry.contents().to_vec())?;
        let stylesheet = Stylesheet::new(path, contents).await?;
        ret.push(stylesheet);
    }
    Ok(ret)
}