/*
 * meli website
 *
 * Copyright 2020 Manos Pitsidianakis
 *
 * This is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with meli website. If not, see <http://www.gnu.org/licenses/>.
 */

extern crate linkify;
use libssg;
use rss;
use std::collections::HashMap;
use std::io::Read;
use std::path::Path;
use std::process::{Command, Stdio};

const RSS_BASE_URL: &'static str = "https://meli.delivery/";
const RSS_AUTHOR_EMAIL: &'static str = "epilys@nessuent.xyz (epilys)";
const RSS_FEED_SNAPSHOT: &'static str = "posts";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut state = libssg::State::new()?;
    state
        .then(libssg::match_pattern(
            "^pages/index.md",
            libssg::Route::Const("index.html".to_string()),
            libssg::Renderer::Pipeline(vec![
                libssg::Renderer::Custom(load_posts()),
                libssg::Renderer::edit_metadata(Box::new(|_state, context| {
                    if let Some(posts) = context.get_mut("posts").and_then(|v| v.as_array_mut()) {
                        posts.truncate(4);
                    }
                    context.insert(
                        "ogImage".to_string(),
                        "/images/screenshots/conversations.webp".to_string().into(),
                    );
                    Ok(Default::default())
                })),
                libssg::Renderer::LoadAndApplyTemplate("templates/index.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))
        .then(libssg::copy("^css/*", libssg::Route::Id))
        .then(libssg::copy("^images/*", libssg::Route::Id))
        .then(libssg::match_pattern(
            "^posts/*",
            libssg::Route::SetExtension("html"),
            libssg::Renderer::Pipeline(vec![
                libssg::Renderer::edit_metadata(Box::new(|_state, context| {
                    context.insert("showTitle".to_string(), libssg::Value::Bool(false));
                    context.insert(
                        "mainClass".to_string(),
                        libssg::Value::String("article".to_string()),
                    );
                    context.insert("ogType".to_string(), "article".to_string().into());
                    Ok(Default::default())
                })),
                libssg::Renderer::LoadAndApplyTemplate("templates/post.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::compiler_seq(
                libssg::pandoc(),
                Box::new(|state, path| {
                    let path = path
                        .strip_prefix(&state.output_dir().parent().unwrap())
                        .unwrap_or(&path)
                        .to_path_buf();
                    if state.verbosity() > 3 {
                        println!("adding {} to RSS snapshot", path.display());
                    }
                    let uuid = libssg::uuid_from_path(&path);
                    state.add_to_snapshot(RSS_FEED_SNAPSHOT.into(), uuid);
                    Ok(Default::default())
                }),
            ),
        ))
        .then(libssg::create(
                "rss.xml".into(),
                libssg::Renderer::Custom(Box::new(|_state, metadata| {
                    Ok(metadata["body"].as_str().unwrap().to_string())
                })),
                Box::new(|state, path| {
                    if !state.snapshots().contains_key(RSS_FEED_SNAPSHOT) {
                        // No posts configured/found
                        return Err(format!("There are no snapshots with key `{}`, is the source rule empty (ie producing no items) or have you typed the name wrong?", &RSS_FEED_SNAPSHOT))?;
                    }

                    let snapshot = &state.snapshots()[RSS_FEED_SNAPSHOT];
                    let mut rss_items = Vec::with_capacity(snapshot.len());
                    for artifact in snapshot.iter() {
                        let rss_item = rss::ItemBuilder::default()
                            .title(state.artifacts()[&artifact].metadata["title"].as_str().unwrap().to_string())
                            .link(format!(
                                "{}{}",
                                RSS_BASE_URL,
                                state.artifacts()[&artifact].path.display()
                            )
                            )
                            .author(RSS_AUTHOR_EMAIL.to_string())
                            .guid(
                                rss::GuidBuilder::default()
                                .value(format!(
                                "{}{}",
                                RSS_BASE_URL,
                                state.artifacts()[&artifact].path.display()
                            )
                            )
                                .permalink(true)
                                .build()
                                .unwrap(),
                            )
                            .pub_date(
                                libssg::chrono::DateTime::parse_from_rfc3339(
                                    state.artifacts()[&artifact].metadata["date_iso_8601"].as_str().unwrap(),
                                )
                                .unwrap()
                                .to_rfc2822(),
                            )
                            .description(String::new())
                            .content(state.artifacts()[&artifact].metadata["body"].as_str().unwrap().to_string())
                            .build()
                            .unwrap();
                        rss_items.push(rss_item);
                    }

                    //<atom:link href="http://dallas.example.com/rss.xml" rel="self" type="application/rss+xml" />
                    let atom_link = [(
                        "link",
                        vec![rss::extension::ExtensionBuilder::default()
                        .name("atom:link".to_string())
                        .attrs(
                            [
                            ("href".to_string(), format!("{}rss.xml", RSS_BASE_URL)),
                            ("rel".to_string(), "self".to_string()),
                            ("type".to_string(), "application/rss+xml".to_string()),
                            ]
                            .iter()
                            .map(|(a, b)| (a.to_string(), b.clone()))
                            .collect::<HashMap<String, String>>(),
                        )
                        .build()
                        .unwrap()],
                    )]
                        .iter()
                        .map(|(a, b)| (a.to_string(), b.clone()))
                        .collect::<HashMap<_, _>>();
                    let mut channel = rss::ChannelBuilder::default()
                        .title("meli MUA")
                        .link("https://meli.delivery")
                        .description("")
                        .namespaces(
                            [
                            ("content", "http://purl.org/rss/1.0/modules/content/"),
                            ("atom", "http://www.w3.org/2005/Atom"),
                            ]
                            .iter()
                            .cloned()
                            .map(|(a, b)| (a.to_string(), b.to_string()))
                            .collect::<std::collections::HashMap<String, String>>(),
                        )
                        .extensions(
                            [("atom", atom_link)]
                            .iter()
                            .cloned()
                            .map(|(a, b)| (a.to_string(), b.clone()))
                            .collect::<std::collections::HashMap<String, _>>(),
                        )
                        .ttl(1800.to_string())
                        .build()
                        .unwrap();
                    channel.set_items(rss_items);
                    let mut writer = vec![];
                    //channel.pretty_write_to(&mut writer, b' ', 2).unwrap();
                    channel.write_to(&mut writer).unwrap();
                    let mut ret = libssg::Map::default();
                    ret.insert("body".to_string(), String::from_utf8(writer)?.into());
                    Ok(ret)
                }),
        ))
            /*
        .then(libssg::build_rss_feed(
            "rss.xml".into(),
            libssg::rss_feed(
                RSS_FEED_SNAPSHOT.into(),
                libssg::RssItem {
                    title: "meli".into(),
                    description: "example using libssg".into(),
                    link: "http://localhost".into(),
                    last_build_date: String::new(),
                    pub_date: libssg::chrono::Local::now().to_rfc2822(),
                    ttl: 1800,
                },
            ),
        ))
        */
        .then(libssg::match_pattern(
            "^pages/documentation.md",
            libssg::Route::Const("documentation.html".to_string()),
            libssg::Renderer::Pipeline(vec![
                default_metadata(),
                libssg::Renderer::edit_metadata(Box::new(|_state, context| {
                    context.insert(
                        "mainClass".to_string(),
                        libssg::Value::String("article-doc".to_string()),
                    );
                    for manpage in &["meli.1", "meli.conf.5", "meli-themes.5"] {
                        let mut cmd = Command::new("./make_manpage.sh");
                        let out = cmd.arg(manpage).output()?.stdout;
                        context.insert(
                            manpage.replace(".", "_"),
                            String::from_utf8_lossy(&out).into(),
                        );
                    }
                    Ok(Default::default())
                })),
                libssg::Renderer::LoadAndApplyTemplate("templates/documentation.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))
        .then(libssg::match_pattern(
            "^pages/download.md",
            libssg::Route::Const("download.html".to_string()),
            libssg::Renderer::Pipeline(vec![
                default_metadata(),
                libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))
        .then(libssg::match_pattern(
            "^pages/screenshots.md",
            libssg::Route::Const("screenshots.html".to_string()),
            libssg::Renderer::Pipeline(vec![
                libssg::Renderer::LoadAndApplyTemplate("templates/screenshots.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))
        .then(libssg::match_pattern(
            "^pages/mailing-lists.md",
            libssg::Route::Const("mailing-lists.html".to_string()),
            libssg::Renderer::Pipeline(vec![
                default_metadata(),
                libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))
        .then(libssg::create(
            "archive.html".into(),
            libssg::Renderer::Pipeline(vec![
                default_metadata(),
                libssg::Renderer::Custom(load_posts()),
                libssg::Renderer::LoadAndApplyTemplate("templates/post-list.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))
        .then(libssg::create(
            "sitemap.html".into(),
            libssg::Renderer::Pipeline(vec![
                default_metadata(),
                libssg::Renderer::LoadAndApplyTemplate("templates/sitemap.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::sitemap(),
        ))
        .finish()?;

    if std::env::var("CHECK").is_ok() {
        use linkify::{LinkFinder, LinkKind};

        fn visit_dirs(dir: &Path) -> std::io::Result<bool> {
            let mut ret = true;
            if dir.is_dir() {
                for entry in std::fs::read_dir(dir)? {
                    let entry = entry?;
                    let path = entry.path();
                    if path.is_dir() {
                        ret = ret && visit_dirs(&path)?;
                    } else {
                        if path.extension() == Some(std::ffi::OsStr::new("html")) {
                            let mut file = std::fs::File::open(&path)?;
                            let finder = LinkFinder::new();
                            let mut r = String::new();
                            file.read_to_string(&mut r)?;
                            for link in finder.links(&r) {
                                if *link.kind() != LinkKind::Url {
                                    continue;
                                }
                                let out = Command::new("curl")
                                    .args(&["--fail", "-s", "-L", link.as_str()])
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::null())
                                    .stderr(Stdio::null())
                                    .spawn()?
                                    .wait()?;
                                if !out.success() {
                                    ret = false;
                                    println!(
                                        "{}: Found dead/invalid outgoing link: {}",
                                        path.display(),
                                        link.as_str()
                                    );
                                }
                            }
                        }
                    }
                }
            }
            Ok(ret)
        }
        visit_dirs(state.output_dir())?;
    }
    Ok(())
}

fn load_posts() -> Box<dyn libssg::BFn> {
    Box::new(|state, context| {
        if !state.snapshots().contains_key(RSS_FEED_SNAPSHOT) {
            // No posts configured/found
            return Err(format!("There are no snapshots with key `{}`, is the source rule empty (ie producing no items) or have you typed the name wrong?", &RSS_FEED_SNAPSHOT))?;
        }

        let snapshot = &state.snapshots()[RSS_FEED_SNAPSHOT];
        let mut items = Vec::with_capacity(snapshot.len());
        for artifact in snapshot.iter() {
            let mut item = state.artifacts()[&artifact].metadata.clone();
            item.insert(
                "url".into(),
                state.artifacts()[&artifact]
                    .path
                    .display()
                    .to_string()
                    .into(),
            );
            items.push(item);
        }
        items.sort_by(|a, b| {
            b["date_iso_8601"]
                .as_str()
                .unwrap()
                .cmp(&a["date_iso_8601"].as_str().unwrap())
        });

        context.insert("posts".into(), items.into());
        Ok(Default::default())
    })
}

fn default_metadata() -> libssg::Renderer {
    libssg::Renderer::edit_metadata(Box::new(|_state, context| {
        context.insert("showTitle".to_string(), libssg::Value::Bool(true));
        context.insert(
            "ogImage".to_string(),
            "/images/screenshots/conversations.webp".to_string().into(),
        );
        context.insert(
            "mainClass".to_string(),
            libssg::Value::String("article".to_string()),
        );
        Ok(Default::default())
    }))
}
