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

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

use crate::{
    dirs::Dirs,
    feed,
    sources::Sources,
    vfs::{self, Content, Dir, Entries, VPath, VPathBuf},
};

use super::BaseTemplateData;

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

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

    fn entries(&self, path: &VPath) -> Vec<VPathBuf> {
        if path == VPath::ROOT {
            let mut entries: Vec<_> = self
                .sources
                .treehouse
                .tags
                .keys()
                .map(|name| VPathBuf::new(format!("{name}.atom")))
                .collect();
            entries.push(VPathBuf::new("new.atom")); // redirect: new -> all
            entries
        } else {
            vec![]
        }
    }

    fn content(&self, path: &VPath) -> Option<Content> {
        if path.extension() == Some("atom") {
            let mut feed_name = path.with_extension("").to_string();
            if feed_name == "new" {
                feed_name = "all".into(); // redirect: new -> all
            }
            if self.sources.treehouse.tags.contains_key(&feed_name) {
                return Some(Content::new(
                    "application/atom+xml",
                    generate_or_error(&self.sources, &self.dirs, &self.handlebars, &feed_name)
                        .into(),
                ));
            }
        }

        None
    }
}

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

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

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

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

#[derive(Serialize)]
struct AtomTemplateData<'a> {
    #[serde(flatten)]
    base: &'a BaseTemplateData<'a>,
    feed: Feed,
    updated: DateTime<Utc>,
}

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

#[instrument(name = "atom::generate", skip(sources, handlebars))]
pub fn generate(
    sources: &Sources,
    dirs: &Dirs,
    handlebars: &Handlebars,
    tag_name: &str,
) -> anyhow::Result<String> {
    let feed = feed::generate(sources, tag_name).ok_or_else(|| anyhow!("feed does not exist"))?; // should not happen in reality; 404 should be returned

    let template_data = AtomTemplateData {
        base: &BaseTemplateData::new(sources),
        feed: Feed {
            name: tag_name.to_owned(),
            entries: feed
                .into_iter()
                .map(|entry| Entry {
                    id: entry.id,
                    updated: entry.updated,
                    url: entry.url,
                    title: entry.title,
                    tags: entry.tags,
                })
                .collect(),
        },
        // The content cache layer should take care of sampling the current time only once,
        // and then preserving it until the treehouse is deployed again.
        updated: Utc::now(),
    };

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