use crate::error::JournalError;
use crate::git;
use crate::template::Templates;
use chrono::{DateTime, Local};
use glob::glob;
use regex::Regex;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tera::Context;

const MAX_DRAFT_COUNT: usize = 1000;

pub struct Journal {
    dirname: PathBuf,
    entries: PathBuf,
    templates: Templates,
}

impl Journal {
    pub fn is_journal(path: &Path, entries: &Path) -> bool {
        is_dir(path) && is_dir(entries)
    }

    pub fn init(path: &Path, entries: &Path) -> Result<Self, JournalError> {
        std::fs::create_dir(path)
            .map_err(|err| JournalError::CreateDirectory(path.to_path_buf(), err))?;
        git::init(path)?;
        std::fs::create_dir(entries)
            .map_err(|err| JournalError::CreateDirectory(entries.to_path_buf(), err))?;
        Ok(Self {
            dirname: path.to_path_buf(),
            entries: entries.to_path_buf(),
            templates: Templates::new(path)?,
        })
    }

    pub fn new(path: &Path, entries: &Path) -> Result<Self, JournalError> {
        if Self::is_journal(path, entries) {
            let dirname = path.to_path_buf();
            let entries = entries.to_path_buf();
            let templates = Templates::new(path)?;
            Ok(Self {
                dirname,
                entries,
                templates,
            })
        } else {
            Err(JournalError::NotAJournal(path.display().to_string()))
        }
    }

    fn dirname(&self) -> &Path {
        &self.dirname
    }

    fn relative(&self, path: &Path) -> Result<PathBuf, JournalError> {
        let path = path.strip_prefix(self.dirname()).map_err(|err| {
            JournalError::RelativePath(path.to_path_buf(), self.dirname().to_path_buf(), err)
        })?;
        Ok(path.to_path_buf())
    }

    fn drafts(&self) -> PathBuf {
        self.dirname().join("drafts")
    }

    fn entries(&self) -> PathBuf {
        self.entries.clone()
    }

    pub fn new_draft(
        &self,
        title: &str,
        topics: &[PathBuf],
        editor: &str,
    ) -> Result<(), JournalError> {
        let drafts = self.drafts();
        if !drafts.exists() {
            std::fs::create_dir(&drafts)
                .map_err(|err| JournalError::CreateDirectory(drafts.to_path_buf(), err))?;
        }

        let mut context = Context::new();
        context.insert("title", title);
        context.insert("date", &current_timestamp());
        let mut full_topics = vec![];
        for topic in topics.iter() {
            let pathname = topic_path(self.dirname(), topic);
            if !pathname.exists() {
                return Err(JournalError::NoSuchTopic(topic.to_path_buf()));
            }
            full_topics.push(topic.display().to_string());
        }
        context.insert("topics", &full_topics);

        let pathname = self.pick_file_id(&drafts)?;
        let text = self.templates.new_draft(&context)?;
        std::fs::write(&pathname, text)
            .map_err(|err| JournalError::WriteEntry(pathname.to_path_buf(), err))?;
        self.edit(editor, &pathname)?;
        Ok(())
    }

    fn pick_file_id(&self, dirname: &Path) -> Result<PathBuf, JournalError> {
        for i in 0..MAX_DRAFT_COUNT {
            let basename = format!("{i}.md");
            let pathname = dirname.join(basename);
            if !pathname.exists() {
                return Ok(pathname);
            }
        }
        Err(JournalError::TooManyDrafts(
            MAX_DRAFT_COUNT,
            dirname.to_path_buf(),
        ))
    }

    pub fn pick_draft(&self, id: &str) -> Result<PathBuf, JournalError> {
        let drafts = self.drafts();
        let filename = drafts.join(format!("{id}.md"));
        if filename.exists() {
            Ok(filename)
        } else {
            Err(JournalError::NoSuchDraft(id.to_string(), self.drafts()))
        }
    }

    pub fn list_drafts(&self) -> Result<(), JournalError> {
        let prefix = format!("{}/", self.drafts().display());
        let pattern = format!("{prefix}*.md");
        let entries =
            glob(&pattern).map_err(|err| JournalError::PatternError(pattern.to_string(), err))?;
        for entry in entries {
            let entry = entry.map_err(|err| JournalError::GlobError(pattern.to_string(), err))?;
            let title = get_title(&entry)?;
            let entry = entry.file_stem().unwrap().to_string_lossy();
            let entry = entry.strip_prefix(&prefix).unwrap_or(&entry);
            println!("{entry} {title}");
        }
        Ok(())
    }

    fn edit(&self, editor: &str, filename: &Path) -> Result<(), JournalError> {
        if editor == "none" {
            return Ok(());
        }
        let mut cmd = Command::new(editor);
        cmd.arg(filename);
        cmd.stderr(Stdio::piped());
        let child = cmd
            .spawn()
            .map_err(|err| JournalError::SpawnEditor(editor.into(), filename.into(), err))?;
        let output = child
            .wait_with_output()
            .map_err(|err| JournalError::SpawnEditor(editor.into(), filename.into(), err))?;
        if output.status.success() {
            Ok(())
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr);
            Err(JournalError::EditorFailed(
                filename.to_path_buf(),
                stderr.into_owned(),
            ))
        }
    }

    pub fn edit_draft(&self, editor: &str, filename: &Path) -> Result<(), JournalError> {
        self.edit(editor, filename)?;
        Ok(())
    }

    pub fn remove_draft(&self, filename: &Path) -> Result<(), JournalError> {
        std::fs::remove_file(filename)
            .map_err(|err| JournalError::RemoveDraft(filename.to_path_buf(), err))?;
        Ok(())
    }

    pub fn finish_draft(&self, filename: &Path, basename: &str) -> Result<(), JournalError> {
        let entries = self.entries();
        if !entries.exists() {
            std::fs::create_dir(&entries)
                .map_err(|err| JournalError::CreateDirectory(entries.to_path_buf(), err))?;
        }

        let subdir = entries.join(Local::now().format("%Y/%m/%d").to_string());
        std::fs::create_dir_all(&subdir)
            .map_err(|err| JournalError::CreateDirectory(entries.to_path_buf(), err))?;

        let basename = PathBuf::from(format!("{basename}.mdwn"));
        let entry = subdir.join(basename);
        std::fs::rename(filename, &entry).map_err(|err| {
            JournalError::RenameEntry(filename.to_path_buf(), entry.to_path_buf(), err)
        })?;

        let entry = self.relative(&entry)?;
        git::add(self.dirname(), &[&entry])?;

        let msg = format!("journal entry {}", entry.display());
        git::commit(self.dirname(), &msg)?;
        Ok(())
    }

    pub fn new_topic(&self, path: &Path, title: &str, editor: &str) -> Result<(), JournalError> {
        let mut context = Context::new();
        context.insert("title", title);

        let dirname = self.dirname();
        if !dirname.exists() {
            return Err(JournalError::NoJournalDirectory(dirname.to_path_buf()));
        }

        let pathname = topic_path(self.dirname(), path);
        let parent = pathname.parent().unwrap();
        if !parent.is_dir() {
            std::fs::create_dir_all(parent)
                .map_err(|err| JournalError::CreateDirectory(parent.to_path_buf(), err))?;
        }

        let text = self.templates.new_topic(&context)?;
        std::fs::write(&pathname, text)
            .map_err(|err| JournalError::WriteTopic(pathname.to_path_buf(), err))?;
        self.edit(editor, &pathname)?;

        let topic = self.relative(&pathname)?;
        git::add(self.dirname(), &[&topic])?;

        let msg = format!("new topic {}", topic.display());
        git::commit(self.dirname(), &msg)?;

        Ok(())
    }
}

fn is_dir(path: &Path) -> bool {
    if let Ok(meta) = std::fs::symlink_metadata(path) {
        meta.is_dir()
    } else {
        false
    }
}

fn topic_path(dirname: &Path, topic: &Path) -> PathBuf {
    dirname.join(format!("{}.mdwn", topic.display()))
}

fn current_timestamp() -> String {
    let now: DateTime<Local> = Local::now();
    now.to_rfc2822()
}

fn get_title(filename: &Path) -> Result<String, JournalError> {
    let text = std::fs::read(filename)
        .map_err(|err| JournalError::ReadDraft(filename.to_path_buf(), err))?;
    let text = String::from_utf8_lossy(&text);
    let pat = Regex::new(r#"^\[\[!meta title="(?P<title>.*)"\]\]"#).unwrap();
    if let Some(caps) = pat.captures(&text) {
        if let Some(m) = caps.name("title") {
            return Ok(m.as_str().to_string());
        }
    }
    Ok("(untitled)".to_string())
}
