use std::{collections::HashMap, fmt, sync::Arc};

use anyhow::{Context, ensure};
use handlebars::Handlebars;
use serde::Serialize;
use tracing::{info_span, instrument};

use crate::{
    dirs::Dirs,
    generate::BaseTemplateData,
    html::{breadcrumbs::breadcrumbs_to_html, tree},
    sources::Sources,
    state::FileId,
    vfs::{self, Content, Dir, Entries, VPath, VPathBuf},
};

#[derive(Serialize)]
struct Page {
    title: String,
    thumbnail: Option<Thumbnail>,
    scripts: Vec<String>,
    styles: Vec<String>,
    breadcrumbs: String,
    tree_path: Option<String>,
    tree: String,
}

#[derive(Serialize)]
struct Thumbnail {
    url: String,
    alt: Option<String>,
}

#[derive(Serialize)]
struct PageTemplateData<'a> {
    #[serde(flatten)]
    base: &'a BaseTemplateData<'a>,
    page: Page,
}

#[instrument(skip(sources, dirs, handlebars))]
pub fn generate(
    sources: &Sources,
    dirs: &Dirs,
    handlebars: &Handlebars,
    file_id: FileId,
) -> anyhow::Result<String> {
    let breadcrumbs = breadcrumbs_to_html(&sources.config, &sources.navigation_map, file_id);

    let roots = sources
        .treehouse
        .roots
        .get(&file_id)
        .expect("tree should have been added to the treehouse");

    let tree = {
        let _span = info_span!("generate_tree::root_to_html").entered();
        let renderer = tree::Renderer {
            sources,
            dirs,
            file_id,
        };
        let mut tree = String::new();
        renderer.root(&mut tree);
        tree
    };

    let template_data = PageTemplateData {
        base: &BaseTemplateData::new(sources),
        page: Page {
            title: roots.attributes.title.clone(),
            thumbnail: roots
                .attributes
                .thumbnail
                .as_ref()
                .map(|thumbnail| Thumbnail {
                    url: sources.config.pic_url(&*dirs.pic, &thumbnail.id),
                    alt: thumbnail.alt.clone(),
                }),
            scripts: roots.attributes.scripts.clone(),
            styles: roots.attributes.styles.clone(),
            breadcrumbs,
            tree_path: sources.treehouse.tree_path(file_id).map(|s| s.to_string()),
            tree,
        },
    };
    let template_name = roots
        .attributes
        .template
        .clone()
        .unwrap_or_else(|| "_tree.hbs".into());

    ensure!(
        handlebars.has_template(&template_name),
        "template {template_name} does not exist"
    );

    let _span = info_span!("handlebars::render").entered();
    handlebars
        .render(&template_name, &template_data)
        .context("template rendering failed")
}

pub fn generate_or_error(
    sources: &Sources,
    dirs: &Dirs,
    handlebars: &Handlebars,
    file_id: FileId,
) -> String {
    match generate(sources, dirs, handlebars, file_id) {
        Ok(html) => html,
        Err(error) => format!("error: {error:?}"),
    }
}

pub struct TreehouseDir {
    dirs: Arc<Dirs>,
    sources: Arc<Sources>,
    handlebars: Arc<Handlebars<'static>>,
    dir_index: DirIndex,
}

impl TreehouseDir {
    pub fn new(
        dirs: Arc<Dirs>,
        sources: Arc<Sources>,
        handlebars: Arc<Handlebars<'static>>,
        dir_index: DirIndex,
    ) -> Self {
        Self {
            dirs,
            sources,
            handlebars,
            dir_index,
        }
    }

    #[instrument("TreehouseDir::dir", skip(self))]
    fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
        // NOTE: This does not include simple templates, because that's not really needed right now.

        let mut index = &self.dir_index;
        for component in path.segments() {
            if let Some(child) = index.children.get(component) {
                index = child;
            } else {
                // There cannot possibly be any entries under an invalid path.
                // Bail early.
                return vec![];
            }
        }

        index
            .children
            .values()
            .map(|child| child.full_path.clone())
            .collect()
    }

    #[instrument("TreehouseDir::content", skip(self))]
    fn content(&self, path: &VPath) -> Option<Content> {
        let path = if path.extension() == Some("tree") {
            path.with_extension("")
        } else {
            path.to_owned()
        };

        let path = if path.is_root() {
            VPath::new_const("index")
        } else {
            &path
        };

        self.sources
            .treehouse
            .files_by_tree_path
            .get(path)
            .map(|&file_id| {
                Content::new(
                    "text/html",
                    generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into(),
                )
            })
    }
}

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

impl fmt::Debug for TreehouseDir {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("TreehouseDir")
    }
}

/// Acceleration structure for `dir` operations on [`TreehouseDir`]s.
#[derive(Debug, Default)]
pub struct DirIndex {
    full_path: VPathBuf,
    children: HashMap<VPathBuf, DirIndex>,
}

impl DirIndex {
    #[instrument(name = "DirIndex::new", skip(paths))]
    pub fn new<'a>(paths: impl Iterator<Item = &'a VPath>) -> Self {
        let mut root = DirIndex::default();

        for path in paths {
            let mut parent = &mut root;
            let mut full_path = VPath::ROOT.to_owned();
            for segment in path.segments() {
                full_path.push(segment);
                let child = parent
                    .children
                    .entry(segment.to_owned())
                    .or_insert_with(|| DirIndex {
                        full_path: full_path.clone(),
                        children: HashMap::new(),
                    });
                parent = child;
            }
        }

        root
    }
}
