use std::{borrow::Cow, ffi::OsStr, path::Path};

use crate::{DOT_GIT_DIR, MODULES};

/// Returns true if the given `git_dir` seems to be a bare repository.
///
/// Please note that repositories without an index generally _look_ bare, even though they might also be uninitialized.
pub fn bare(git_dir_candidate: &Path) -> bool {
    !(git_dir_candidate.join("index").exists() || (git_dir_candidate.file_name() == Some(OsStr::new(DOT_GIT_DIR))))
}

/// Parse `<git_dir_candidate>/config` quickly to evaluate the value of the `bare` line, or return `true` if the file doesn't exist
/// similar to what`guess_repository_type` seems to be doing.
/// Return `None` if the `bare` line can't be found or the value of `bare` can't be determined.
fn bare_by_config(git_dir_candidate: &Path) -> std::io::Result<Option<bool>> {
    match std::fs::read(git_dir_candidate.join("config")) {
        Ok(buf) => Ok(config::parse_bare(&buf)),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Some(true)),
        Err(err) => Err(err),
    }
}

// Copied and adapted from `gix-config-value::boolean`.
mod config {
    use bstr::{BStr, ByteSlice};

    /// Note that we intentionally turn repositories that have a worktree configuration into bare repos,
    /// as we don't actually parse the worktree from the config file and expect the caller to do the right
    /// think when seemingly seeing bare repository.
    /// The reason we do this is to not incorrectly pretend this is a worktree.
    pub(crate) fn parse_bare(buf: &[u8]) -> Option<bool> {
        let mut is_bare = None;
        let mut has_worktree_configuration = false;
        for line in buf.lines() {
            if is_bare.is_none() {
                if let Some(line) = line.trim().strip_prefix(b"bare") {
                    is_bare = match line.first() {
                        None => Some(true),
                        Some(c) if *c == b'=' => parse_bool(line.get(1..)?.trim_start().as_bstr()),
                        Some(c) if c.is_ascii_whitespace() => match line.split_once_str(b"=") {
                            Some((_left, right)) => parse_bool(right.trim_start().as_bstr()),
                            None => Some(true),
                        },
                        Some(_other_char_) => None,
                    };
                    continue;
                }
            }
            if line.trim().strip_prefix(b"worktree").is_some() {
                has_worktree_configuration = true;
                break;
            }
        }
        is_bare.map(|bare| bare || has_worktree_configuration)
    }

    fn parse_bool(value: &BStr) -> Option<bool> {
        Some(if parse_true(value) {
            true
        } else if parse_false(value) {
            false
        } else {
            use std::str::FromStr;
            if let Some(integer) = value.to_str().ok().and_then(|s| i64::from_str(s).ok()) {
                integer != 0
            } else {
                return None;
            }
        })
    }

    fn parse_true(value: &BStr) -> bool {
        value.eq_ignore_ascii_case(b"yes") || value.eq_ignore_ascii_case(b"on") || value.eq_ignore_ascii_case(b"true")
    }

    fn parse_false(value: &BStr) -> bool {
        value.eq_ignore_ascii_case(b"no")
            || value.eq_ignore_ascii_case(b"off")
            || value.eq_ignore_ascii_case(b"false")
            || value.is_empty()
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn various() {
            for (input, expected) in [
                ("bare=true", Some(true)),
                ("bare=1", Some(true)),
                ("bare =1", Some(true)),
                ("bare= yes", Some(true)),
                ("bare=false", Some(false)),
                ("bare=0", Some(false)),
                ("bare=blah", None),
                ("bare=", Some(false)),
                ("bare=  \n", Some(false)),
                ("bare = true \n", Some(true)),
                ("\t bare = false \n", Some(false)),
                ("\n\tbare=true", Some(true)),
                ("\n\tbare=true\n\tfoo", Some(true)),
                ("\n\tbare ", Some(true)),
                ("\n\tbare", Some(true)),
                ("not found\nreally", None),
            ] {
                assert_eq!(parse_bare(input.as_bytes()), expected, "{input:?}");
            }
        }
    }
}

/// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone.
pub fn submodule_git_dir(git_dir: &Path) -> bool {
    let mut last_comp = None;
    git_dir.file_name() != Some(OsStr::new(DOT_GIT_DIR))
        && git_dir.components().rev().skip(1).any(|c| {
            if c.as_os_str() == OsStr::new(DOT_GIT_DIR) {
                true
            } else {
                last_comp = Some(c.as_os_str());
                false
            }
        })
        && last_comp == Some(OsStr::new(MODULES))
}

/// What constitutes a valid git repository, returning the guessed repository kind
/// purely based on the presence of files. Note that the git-config ultimately decides what's bare.
///
/// Returns the `Kind` of git directory that was passed, possibly alongside the supporting private worktree git dir.
///
/// Note that `.git` files are followed to a valid git directory, which then requires…
///
///   * …a valid head
///   * …an objects directory
///   * …a refs directory
///
pub fn git(git_dir: &Path) -> Result<crate::repository::Kind, crate::is_git::Error> {
    let git_dir_metadata = git_dir.metadata().map_err(|err| crate::is_git::Error::Metadata {
        source: err,
        path: git_dir.into(),
    })?;
    git_with_metadata(git_dir, git_dir_metadata)
}

pub(crate) fn git_with_metadata(
    git_dir: &Path,
    git_dir_metadata: std::fs::Metadata,
) -> Result<crate::repository::Kind, crate::is_git::Error> {
    #[derive(Eq, PartialEq)]
    enum Kind {
        MaybeRepo,
        Submodule,
        LinkedWorkTreeDir,
        WorkTreeGitDir { work_dir: std::path::PathBuf },
    }

    let dot_git = if git_dir_metadata.is_file() {
        let private_git_dir = crate::path::from_gitdir_file(git_dir)?;
        Cow::Owned(private_git_dir)
    } else {
        Cow::Borrowed(git_dir)
    };

    {
        // Fast-path: avoid doing the complete search if HEAD is already not there.
        // TODO(reftable): use a ref-store to lookup HEAD if ref-tables should be supported, or detect ref-tables beforehand.
        if !dot_git.join("HEAD").exists() {
            return Err(crate::is_git::Error::MissingHead);
        }
        // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here.
        // With ref-table, the has is probably stored as part of the ref-db itself, so we can handle it from there.
        // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly.
        let refs = gix_ref::file::Store::at(dot_git.as_ref().into(), Default::default());
        let head = refs.find_loose("HEAD")?;
        if head.name.as_bstr() != "HEAD" {
            return Err(crate::is_git::Error::MisplacedHead {
                name: head.name.into_inner(),
            });
        }
    }

    let (common_dir, kind) = if git_dir_metadata.is_file() {
        let common_dir = dot_git.join("commondir");
        match crate::path::from_plain_file(&common_dir) {
            Some(Err(err)) => {
                return Err(crate::is_git::Error::MissingCommonDir {
                    missing: common_dir,
                    source: err,
                })
            }
            Some(Ok(common_dir)) => {
                let common_dir = dot_git.join(common_dir);
                (Cow::Owned(common_dir), Kind::LinkedWorkTreeDir)
            }
            None => (dot_git.clone(), Kind::Submodule),
        }
    } else {
        let common_dir = dot_git.join("commondir");
        let worktree_and_common_dir = crate::path::from_plain_file(&common_dir)
            .and_then(Result::ok)
            .and_then(|cd| {
                crate::path::from_plain_file(&dot_git.join("gitdir"))
                    .and_then(Result::ok)
                    .map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd))
            });
        match worktree_and_common_dir {
            Some((work_dir, common_dir)) => {
                let common_dir = dot_git.join(common_dir);
                (Cow::Owned(common_dir), Kind::WorkTreeGitDir { work_dir })
            }
            None => (dot_git.clone(), Kind::MaybeRepo),
        }
    };

    {
        let objects_path = common_dir.join("objects");
        if !objects_path.is_dir() {
            return Err(crate::is_git::Error::MissingObjectsDirectory { missing: objects_path });
        }
    }
    {
        let refs_path = common_dir.join("refs");
        if !refs_path.is_dir() {
            return Err(crate::is_git::Error::MissingRefsDirectory { missing: refs_path });
        }
    }
    Ok(match kind {
        Kind::LinkedWorkTreeDir => crate::repository::Kind::WorkTree {
            linked_git_dir: Some(dot_git.into_owned()),
        },
        Kind::WorkTreeGitDir { work_dir } => crate::repository::Kind::WorkTreeGitDir { work_dir },
        Kind::Submodule => crate::repository::Kind::Submodule {
            git_dir: dot_git.into_owned(),
        },
        Kind::MaybeRepo => {
            let conformed_git_dir = if git_dir == Path::new(".") {
                gix_path::realpath(git_dir)
                    .map(Cow::Owned)
                    .unwrap_or(Cow::Borrowed(git_dir))
            } else {
                Cow::Borrowed(git_dir)
            };
            if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) {
                crate::repository::Kind::PossiblyBare
            } else if submodule_git_dir(conformed_git_dir.as_ref()) {
                crate::repository::Kind::SubmoduleGitDir
            } else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR))
                || !bare_by_config(conformed_git_dir.as_ref())
                    .map_err(|err| crate::is_git::Error::Metadata {
                        source: err,
                        path: conformed_git_dir.join("config"),
                    })?
                    .ok_or(crate::is_git::Error::Inconclusive)?
            {
                crate::repository::Kind::WorkTree { linked_git_dir: None }
            } else {
                crate::repository::Kind::PossiblyBare
            }
        }
    })
}
