use std::{
    fmt::{self},
    sync::Arc,
};

use anyhow::{Context, anyhow};
use chrono::{DateTime, Utc};
use handlebars::Handlebars;
use serde::Serialize;
use tracing::{error, instrument};

use crate::{
    dirs::Dirs,
    doc::Doc,
    feed,
    generate::BaseTemplateData,
    html::djot,
    sources::Sources,
    state::FileId,
    vfs::{Content, Dir, Query, VPath},
};

#[derive(Serialize)]
struct Page {
    title: String,
    thumbnail: Option<Thumbnail>,
    scripts: Vec<String>,
    styles: Vec<String>,
    tree_path: String,
    doc: String,
    feed: Option<Feed>,
    updated: Option<DateTime<Utc>>,
    tags: Vec<String>,
}

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

#[derive(Serialize)]
struct Feed {
    title: String,
    entries: Vec<Entry>,
}

#[derive(Serialize)]
struct Entry {
    title: String,
    url: String,
    updated: Option<DateTime<Utc>>,
    tags: Vec<String>,
}

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

pub struct DocDir {
    pub sources: Arc<Sources>,
    pub dirs: Arc<Dirs>,

    pub handlebars: Arc<Handlebars<'static>>,
}

impl DocDir {
    #[instrument("DocDir::content", skip(self))]
    pub fn content(&self, path: &VPath) -> Option<Content> {
        if !matches!(path.extension(), None | Some("dj")) {
            return None;
        }

        if let Some(file_id) = self
            .sources
            .treehouse
            .files_by_doc_path
            .get(&path.with_extension("dj"))
            && let Some(doc) = self.sources.treehouse.docs.get(file_id)
        {
            return Some(Content::new(
                "text/html",
                self.generate(*file_id, path, doc).into_bytes(),
            ));
        }

        None
    }

    fn generate(&self, file_id: FileId, path: &VPath, doc: &Doc) -> String {
        let events: Vec<_> = jotdown::Parser::new(&doc.text).into_offset_iter().collect();
        let mut rendered_markup = String::new();
        let render_diagnostics = djot::Renderer {
            config: &self.sources.config,
            dirs: &self.dirs,
            treehouse: &self.sources.treehouse,
            file_id,
            page_id: path.to_string(),
        }
        .render(&events, &mut rendered_markup);

        let template_name = doc.attributes.template.as_deref().unwrap_or("_doc.hbs");

        let render_result = self
            .handlebars
            .render(
                template_name,
                &PageTemplateData {
                    base: &BaseTemplateData::new(&self.sources),
                    page: Page {
                        title: doc.attributes.title.clone(),
                        thumbnail: doc.attributes.thumbnail.as_ref().map(|pic| Thumbnail {
                            url: self.sources.config.pic_url(&*self.dirs.pic, &pic.id),
                            alt: pic.alt.clone(),
                        }),
                        scripts: doc.attributes.scripts.clone(),
                        styles: doc.attributes.styles.clone(),
                        tree_path: path.to_string(),
                        doc: rendered_markup,
                        feed: doc.attributes.include_feed.as_ref().and_then(|feed| {
                            Some(Feed {
                                title: feed.title.clone(),
                                entries: self
                                    .generate_feed(&feed.tag)
                                    .inspect_err(|e| {
                                        error!("generating feed for {path} failed: {e}")
                                    })
                                    .ok()?,
                            })
                        }),
                        updated: doc.attributes.updated,
                        tags: doc.attributes.tags.clone(),
                    },
                },
            )
            .context("template rendering failed");
        match render_result {
            Ok(rendered) => rendered,
            Err(error) => format!("{error:#?}"),
        }
    }

    fn generate_feed(&self, tag_name: &str) -> anyhow::Result<Vec<Entry>> {
        let feed = feed::generate(&self.sources, tag_name)
            .ok_or_else(|| anyhow!("tag #{tag_name} doesn't exist"))?;
        Ok(feed
            .into_iter()
            .map(|entry| Entry {
                title: entry.title,
                url: entry.url,
                updated: entry.updated,
                tags: {
                    let mut tags = entry.tags;
                    // Don't show the "self" tag in the list.
                    if let Some(i) = tags.iter().position(|x| x == tag_name) {
                        tags.remove(i);
                    }
                    tags
                },
            })
            .collect())
    }
}

impl Dir for DocDir {
    fn query(&self, path: &VPath, query: &mut Query) {
        query.try_provide(|| self.content(path));
    }
}

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