// Copyright (C) 2022-2023 Enrico Guiraud
//
// This file is part of Kobold, a static site generator with a friendly CLI.
//
// Kobold 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.
//
// Kobold 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 Kobold. If not, see <http://www.gnu.org/licenses/>.

use crate::log;
use colorful::Colorful; // bold
use log::Lvl;
use std::borrow::Cow;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Output};
use std::str;

type Result<T> = std::result::Result<T, Cow<'static, str>>;

const KOBOLD_DIRS: [&str; 4] = ["content", "layouts", "css", "macros"];
const DEFAULT_INDEX: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/assets/default_index.md"
));
const DEFAULT_PAGE_LAYOUT: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/assets/layouts/default.html"
));
const MVP_CSS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/css/mvp.css"));

/// Create a new directory `dir` and populate it with Kobold's directory structure:
///
/// dir/
/// ├── content/
/// │   └── index.md
/// ├── layouts/
/// │   └── default.html
/// ├── macros/
/// │   └── ...
/// └── css/
///     └── ...
///
/// A new git repository is also initialized at `dir` for convenience.
pub fn init(dir: &Path) -> Result<()> {
    // assert directory does not already exist
    if Path::new(&dir).exists() {
        Err(format!("Directory `{}` already exists.", dir.display()))?;
        // TODO "...and it (looks|does not look) like a Kobold website" (can use
        // assert_valid_kobold_project)
    }

    create_dir_structure(dir)?;
    write_default_index(&dir.join("content").join("index.md"))?;
    write_default_assets(dir)?;
    try_git_init(dir);

    Ok(())
}

/// Create the directory structure of a typical kobold website at `dir`.
fn create_dir_structure(dir: &Path) -> Result<()> {
    // create website directory
    fs::create_dir_all(dir)
        .map_err(|msg| format!("Could not create directory `{}`:\n\t{}. Make sure you have write permissions to the directory (and the disk is not full).", dir.display(), msg))?;

    // create kobold sub-directories
    for subdir in KOBOLD_DIRS {
        fs::create_dir(format!("{}/{}", dir.display(), subdir)).map_err(|msg| {
            format!(
                "Could not create directory `{}/{}`:\n\t{}",
                dir.display(),
                subdir,
                msg
            )
        })?;
    }

    Ok(())
}

fn write_default_index(fname: &Path) -> Result<()> {
    let mut f = fs::File::create(fname).map_err(|msg| {
        format!(
            "Could not create default index `{}`:\n\t{}",
            fname.display(),
            msg
        )
    })?;

    write!(&mut f, "{}", DEFAULT_INDEX).map_err(|msg| {
        format!(
            "Could not write default index contents to file `{}`:\n\t{}",
            fname.display(),
            msg
        )
    })?;

    Ok(())
}

/// Write out default assets into new kobold site.
fn write_default_assets(to_dir: &Path) -> Result<()> {
    // write default page layout
    let fname = to_dir.join("layouts").join("default.html");
    let mut f = fs::File::create(&fname).map_err(|msg| {
        format!(
            "Could not create default page layout `{}`:\n\t{}",
            fname.display(),
            msg
        )
    })?;
    write!(&mut f, "{}", DEFAULT_PAGE_LAYOUT).map_err(|msg| {
        format!(
            "Could not write default page layout to file `{}`:\n\t{}",
            fname.display(),
            msg
        )
    })?;

    // write default CSS (mvp.css)
    let fname = to_dir.join("css").join("mvp.css");
    let mut f = fs::File::create(&fname).map_err(|msg| {
        format!(
            "Could not create default CSS file `{}`:\n\t{}",
            fname.display(),
            msg
        )
    })?;
    write!(&mut f, "{}", MVP_CSS).map_err(|msg| {
        format!(
            "Could not write default CSS file contents to `{}`:\n\t{}",
            fname.display(),
            msg
        )
    })?;

    Ok(())
}

/// Initialize git repository at `dir`, print warning on failure.
fn try_git_init(dir: &Path) {
    let dir_as_str = dir.to_str().unwrap_or_else(|| {
        panic!(
            "The directory to initialize, `{}`, cannot be represented as a string!",
            dir.display()
        )
    });
    let r = Command::new("git").args(["init", dir_as_str]).output();

    match r {
        Err(err) => {
            let msg = format!(
                "Could not initialize a git repository at `{}`:\n\t{}",
                dir.display(),
                &err
            );
            log::log(Lvl::Warning, &msg);
        }
        Ok(Output {
            status,
            stdout,
            stderr,
        }) if !status.success() => {
            let msg = format!(
                "Could not initialize a git repository at `{}`:\n\t{} {}\n\t{} {}",
                dir.display(),
                "StdOut:".bold().color(Lvl::Info.as_color()),
                str::from_utf8(&stdout).unwrap_or("<OUTPUT WAS NOT VALID UTF-8>"),
                "StdErr:".bold().color(Lvl::Info.as_color()),
                str::from_utf8(&stderr).unwrap_or("<OUTPUT WAS NOT VALID UTF-8>"),
            );
            log::log(Lvl::Warning, &msg);
        }
        _ => (),
    }
}

#[cfg(test)]
mod tests {
    use assert_fs::fixture::{NamedTempFile, TempDir};
    use assert_fs::prelude::*;
    use predicates::prelude::*;

    #[test]
    fn good_init() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let tmp_dir = TempDir::new().unwrap();
        let site_dir = tmp_dir.join("test_site");

        super::init(&site_dir)?;

        let site_dir = tmp_dir.child("test_site");
        site_dir.assert(predicate::path::is_dir());
        for subdir in super::KOBOLD_DIRS {
            site_dir.child(subdir).assert(predicate::path::is_dir());
        }
        site_dir.child(".git").assert(predicate::path::is_dir());

        site_dir
            .child("content")
            .child("index.md")
            .assert(String::from(super::DEFAULT_INDEX));

        assert!(site_dir.child("layouts").child("default.html").exists());

        Ok(())
    }

    #[test]
    fn bad_init_path() -> Result<(), Box<dyn std::error::Error>> {
        let not_a_dir = NamedTempFile::new("not_a_directory")?;
        not_a_dir.touch()?;

        match super::init(&not_a_dir.path().join("site_dir")) {
            Ok(()) => Err("This should have failed."),
            Err(msg) => {
                let expected_msg = "Could not create directory";
                assert!(msg.contains(expected_msg));
                Ok(())
            }
        }?;

        Ok(())
    }

    #[test]
    fn existing_init_path() -> Result<(), Box<dyn std::error::Error>> {
        let tmp_dir = TempDir::new().unwrap();

        match super::init(tmp_dir.path()) {
            Ok(()) => Err("This should have failed"),
            Err(msg) => {
                let expected_msg = format!("Directory `{}` already exists", tmp_dir.display());
                assert!(msg.contains(&expected_msg));
                Ok(())
            }
        }?;

        Ok(())
    }
}
