use std::fs;
use std::path::Path;
use std::time::Instant;

use anyhow::Result;
use chrono::NaiveDate;
use pulldown_cmark::{Options, Parser, html};
use serde::Serialize;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use tera::{Context, Tera};
use yaml_rust2::YamlLoader;

use crate::cli::Site;
use super::{SyntaxHighlighter, copy_dir, english_months, french_months, remove_build_dir, write_file};
use super::trips::load_trips;

#[derive(Serialize, Debug)]
struct Content {
    filename: String,
    url: String,

    title: String,
    description: String,

    markdown: String,
    html: String,

    date: chrono::DateTime<chrono::Utc>,
    date_fr: String,
    date_en: String,
    date_rss: String,

    lang: String,

    slides: Option<String>,
    video: Option<String>,
    tweet: Option<String>,
}

pub fn run(site: Site) -> Result<()> {
    let start = Instant::now();

    remove_build_dir(site);

    let mut posts = get_contents("posts");
    let talks = get_contents("talks");

    let tera = Tera::new("content/templates/**/*.html")
        .unwrap_or_else(|e| panic!("Parsing error(s): {}", e));

    let syntax_highlighter = SyntaxHighlighter::new();

    for post in &posts {
        write_content(site, post, &tera, &syntax_highlighter);
    }

    posts = posts
        .into_iter()
        .filter(|post| post.date < chrono::Utc::now())
        .collect();

    let mut context = Context::new();
    context.insert("posts", &posts);
    context.insert("title", "Mes derniers articles");
    context.insert("blog", &true);
    let index = tera.render("index.html", &context).unwrap();
    write_file(site, "index.html", index);

    let mut context = Context::new();
    context.insert("posts", &posts);
    context.insert("latest_post", &posts.last().unwrap());
    let feed = tera.render("rss.xml.html", &context).unwrap();
    write_file(site, "feed.xml", feed);

    let mut context = Context::new();
    context.insert("title", "À propos");
    context.insert("about", &true);
    let about = tera.render("about.html", &context).unwrap();
    write_file(site, "about.html", about);

    let mut context = Context::new();
    context.insert("title", "Mes dernières conférences");
    context.insert("talks", &talks);
    let talks_html = tera.render("talks.html", &context).unwrap();
    write_file(site, "talks.html", talks_html);

    let mut context = Context::new();
    context.insert("title", "Mes dernières randonnées à pied et à vélo");
    context.insert("trips", &load_trips());
    context.insert("hiking", &true);
    let hiking = tera.render("hiking.html", &context).unwrap();
    write_file(site, "hiking.html", hiking);

    copy_dir(site, "./content/images", "images");
    copy_dir(site, "./content/videos", "videos");
    copy_dir(site, "./content/traces", "traces");
    copy_dir(site, "./content/talks", "talks");
    copy_dir(site, "./node_modules/typeface-merriweather/files", "css/files");

    println!("Build terminé en {:?}", start.elapsed());

    Ok(())
}

fn get_contents(directory: &str) -> Vec<Content> {
    let mut contents: Vec<Content> = fs::read_dir(format!("content/{directory}"))
        .unwrap()
        .map(|entry| entry.unwrap().path())
        .filter(|path| path.is_file())
        .map(|path| get_content(directory, &path))
        .collect();
    contents.sort_by_key(|content| content.filename.clone());
    contents
}

fn get_content(prefix: &str, path: &Path) -> Content {
    let filename = path.file_name().unwrap().to_str().unwrap().to_string();
    let date_parts: Vec<&str> = filename.split('-').collect();

    let raw = fs::read_to_string(path)
        .unwrap_or_else(|e| panic!("Cannot read {}: {e}", path.display()));
    let mut blocks: Vec<&str> = raw.split("---").collect();
    blocks.remove(0);
    let metadata = &YamlLoader::load_from_str(blocks.first().unwrap())
        .unwrap_or_else(|e| panic!("Cannot parse YAML in {filename}: {e}"))[0];
    blocks.remove(0);
    let markdown = blocks.join("---").to_string();

    let mut options = Options::empty();
    options.insert(Options::ENABLE_STRIKETHROUGH);
    options.insert(Options::ENABLE_TABLES);
    let parser = Parser::new_ext(&markdown, options);

    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);

    let year: i32 = date_parts[0].parse().unwrap();
    let month: u32 = date_parts[1].trim_start_matches('0').parse().unwrap();
    let day: u32 = date_parts[2].trim_start_matches('0').parse().unwrap();

    let date = NaiveDate::from_ymd_opt(year, month, day)
        .unwrap()
        .and_hms_opt(0, 0, 0)
        .unwrap()
        .and_utc();

    Content {
        filename: filename.clone(),
        url: format!("/{prefix}/{}", filename.replace(".md", ".html")),
        title: metadata["title"].as_str().unwrap().to_string(),
        description: metadata["description"].as_str().unwrap_or("").to_string(),
        markdown,
        html: html_output,
        date,
        date_fr: format!(
            "{} {} {}",
            date_parts[2].trim_start_matches('0'),
            french_months(date_parts[1]),
            date_parts[0]
        ),
        date_en: format!(
            "{} {}, {}",
            english_months(date_parts[1]),
            date_parts[2].trim_start_matches('0'),
            date_parts[0]
        ),
        date_rss: date.to_rfc2822(),
        lang: metadata["lang"].as_str().unwrap_or("fr").to_string(),
        slides: metadata["slides"].as_str().map(|s| s.to_string()),
        video: metadata["video"].as_str().map(|s| s.to_string()),
        tweet: metadata["tweet"].as_str().map(|s| s.to_string()),
    }
}

fn write_content(site: Site, content: &Content, tera: &Tera, syntax_highlighter: &SyntaxHighlighter) {
    let mut context = Context::new();
    context.insert("post", content);
    context.insert("title", &content.title);
    context.insert("blog", &true);
    context.insert("description", &content.description);
    let mut post_html = tera.render("post.html", &context).unwrap();

    let markdown_captures: Vec<_> = syntax_highlighter
        .markdown_regexp
        .captures_iter(&content.markdown)
        .collect();

    for (i, found_in_markdown) in markdown_captures.iter().enumerate() {
        let found_in_html = syntax_highlighter
            .html_regexp
            .captures_iter(&post_html)
            .nth(i)
            .unwrap();

        let lang = found_in_markdown.get(1).unwrap().as_str();

        if lang.is_empty() {
            continue;
        }

        let code_in_markdown = found_in_markdown.get(2).unwrap();
        let code_in_html = found_in_html.get(3).unwrap();

        let syntax = syntax_highlighter
            .ps
            .find_syntax_by_extension(lang)
            .unwrap_or_else(|| panic!("Cannot find syntax for '{lang}'"));

        let mut code = code_in_markdown.as_str().to_string();
        let mut without_php_opening = false;

        if !code.starts_with("<?php") && lang == "php" {
            without_php_opening = true;
            code = format!("<?php\n{}", code);
        }

        if lang == "hs" {
            code = format!("module Example where\n\n{}", code);
        }

        let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
            syntax,
            &syntax_highlighter.ps,
            ClassStyle::SpacedPrefixed { prefix: "syntect" },
        );
        for line in code.lines() {
            html_generator
                .parse_html_for_line_which_includes_newline(&format!("{line}\n"))
                .unwrap();
        }
        let mut output_html = html_generator.finalize();

        if without_php_opening {
            let end_of_first_line = output_html.find('\n').unwrap() + 1;
            output_html.replace_range(..end_of_first_line, "");
        }

        post_html.replace_range(code_in_html.range(), &output_html);
    }

    write_file(site, content.url.trim_start_matches('/'), post_html);
}
