use std::fs::File;

use camino::Utf8Path;
use jiff::Timestamp;

#[cfg(feature = "git")]
static CREATED: &str = r#"git log --reverse -1 --pretty="format:%cI" --"#;
#[cfg(feature = "git")]
static MODIFIED: &str = r#"git log -1 --pretty="format:%cI" --"#;

#[cfg(feature = "git")]
fn run(arg: &str) -> Result<Option<Timestamp>, Error> {
    use std::process::Command;

    if which::which("git").is_err() {
        return Ok(None);
    }

    let (shell, cmd) = if cfg!(target_os = "windows") {
        ("cmd", "/C")
    } else {
        ("sh", "-s")
    };

    tracing::debug!(cmd=?arg, "running git command");

    let output = Command::new(shell)
        .arg(cmd)
        .arg(arg)
        .output()
        .map_err(Error::Command)?;

    if !output.status.success() {
        return Err(Error::CommandFailed(
            String::from_utf8_lossy(&output.stdout).to_string(),
        ));
    }

    let output = String::from_utf8(output.stdout).map_err(Error::OutputUtf8)?;
    let output = output.trim();

    // TODO: should this be an error, it catches git the file doesnt have an associated commit
    if output.is_empty() || output.starts_with("fatal") {
        return Ok(None);
    }

    let timestamp = output.parse().map_err(Error::TimestampParse)?;

    Ok(Some(timestamp))
}

#[derive(Debug, miette::Diagnostic, thiserror::Error)]
pub enum Error {
    #[error("failed to open file")]
    FileOpen(#[source] std::io::Error),
    #[error("failed to get metadata of file")]
    FileMetadata(#[source] std::io::Error),
    #[error("failed to get timestamp of file")]
    FileTime(#[source] std::io::Error),
    #[error("failed to convert timestamp from system time of file")]
    TimestampConvert(#[source] jiff::Error),

    #[cfg(feature = "git")]
    #[error("failed to run command against file")]
    Command(#[source] std::io::Error),
    #[cfg(feature = "git")]
    #[error("failed to run command against file: {0}")]
    CommandFailed(String),
    #[cfg(feature = "git")]
    #[error("failed to convert command output to utf-8 for file")]
    OutputUtf8(#[source] std::string::FromUtf8Error),
    #[cfg(feature = "git")]
    #[error("failed to parse timestamp of file")]
    TimestampParse(#[source] jiff::Error),
}

/// Metadata of an arbitrary Vault file.
///
/// Currently only stores the file's creation and modification times.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct FileMetadata {
    /// The date the file was created according to the file system.
    ///
    /// This may not be correct.
    pub created: Timestamp,
    /// The date the file was modified according to the file system.
    ///
    /// This may not be correct.
    pub modified: Timestamp,

    /// The date the file was created according to Git's commit history.
    pub git_created: Option<Timestamp>,
    /// The date the file was created modified to Git's commit history.
    pub git_modified: Option<Timestamp>,
}

impl FileMetadata {
    pub fn new(path: &Utf8Path) -> Result<Self, Error> {
        let file = File::open(path.as_std_path()).map_err(Error::FileOpen)?;
        let meta = file.metadata().map_err(Error::FileMetadata)?;

        let created = meta.created().map_err(Error::FileTime)?;
        let created = Timestamp::try_from(created).map_err(Error::TimestampConvert)?;

        let modified = meta.modified().map_err(Error::FileTime)?;
        let modified = Timestamp::try_from(modified).map_err(Error::TimestampConvert)?;

        #[cfg(not(feature = "git"))]
        let git_created = None;
        #[cfg(not(feature = "git"))]
        let git_modified = None;

        #[cfg(feature = "git")]
        let git_created = run(&format!("{CREATED} {path}"))?;
        #[cfg(feature = "git")]
        let git_modified = run(&format!("{MODIFIED} {path}"))?;

        Ok(Self {
            created,
            modified,

            git_created,
            git_modified,
        })
    }
}

#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Heading {
    pub level: usize,
    pub id: String,
    pub title: String,
    pub children: Vec<Self>,
}

pub fn extract_headings(contents: &str) -> Vec<Heading> {
    let mut headings = Vec::new();
    let mut in_code_block = false;

    for line in contents.lines() {
        if line.trim_start().starts_with("```") {
            in_code_block = !in_code_block;

            continue;
        }

        if in_code_block {
            continue;
        }

        if line.starts_with('#') {
            let level = line.chars().take_while(|&c| c == '#').count();
            let title = line[level..].trim().to_string();

            headings.push(Heading {
                level,
                id: String::new(),
                title,
                children: vec![],
            });
        }
    }

    headings
}

// Header code taken from Zola
/// Converts the flat temp headings into a nested set of headings
/// representing the hierarchy
pub fn make_table_of_contents(headings: Vec<Heading>) -> Vec<Heading> {
    let mut toc = vec![];
    for heading in headings {
        // First heading or we try to insert the current heading in a previous one
        if toc.is_empty() || !insert_into_parent(toc.iter_mut().last(), &heading) {
            toc.push(heading);
        }
    }

    toc
}

// Takes a potential (mutable) parent and a heading to try and insert into
// Returns true when it performed the insertion, false otherwise
fn insert_into_parent(potential_parent: Option<&mut Heading>, heading: &Heading) -> bool {
    match potential_parent {
        None => {
            // No potential parent to insert into so it needs to be insert higher
            false
        }
        Some(parent) => {
            if heading.level <= parent.level {
                // Heading is same level or higher so we don't insert here
                return false;
            }
            if heading.level + 1 == parent.level {
                // We have a direct child of the parent
                parent.children.push(heading.clone());
                return true;
            }
            // We need to go deeper
            if !insert_into_parent(parent.children.iter_mut().last(), heading) {
                // No, we need to insert it here
                parent.children.push(heading.clone());
            }
            true
        }
    }
}
