use gix::worktree::IndexPersistedOrInMemory;

use crate::OutputFormat;

pub struct Options {
    pub format: OutputFormat,
    pub statistics: bool,
}

pub(crate) mod function {
    use std::{borrow::Cow, io, path::Path};

    use anyhow::bail;
    use gix::bstr::BStr;

    use crate::{
        OutputFormat, is_dir_to_mode,
        repository::{
            PathsOrPatterns,
            attributes::query::{Options, attributes_cache},
        },
    };

    pub fn query(
        repo: gix::Repository,
        input: PathsOrPatterns,
        mut out: impl io::Write,
        mut err: impl io::Write,
        Options { format, statistics }: Options,
    ) -> anyhow::Result<()> {
        if format != OutputFormat::Human {
            bail!("JSON output isn't implemented yet");
        }

        let (mut cache, index) = attributes_cache(&repo)?;
        let mut matches = cache.attribute_matches();

        match input {
            PathsOrPatterns::Paths(paths) => {
                for path in paths {
                    let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref()))
                        .metadata()
                        .ok()
                        .map(|m| is_dir_to_mode(m.is_dir()));

                    let entry = cache.at_entry(&path, mode)?;
                    if !entry.matching_attributes(&mut matches) {
                        continue;
                    }
                    print_match(&matches, path.as_ref(), &mut out)?;
                }
            }
            PathsOrPatterns::Patterns(patterns) => {
                let mut pathspec = repo.pathspec(
                    true,
                    patterns.iter(),
                    true,
                    &index,
                    gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
                        .adjust_for_bare(repo.is_bare()),
                )?;
                let mut pathspec_matched_entry = false;
                if let Some(it) = pathspec.index_entries_with_paths(&index) {
                    for (path, entry) in it {
                        pathspec_matched_entry = true;
                        let entry = cache.at_entry(path, entry.mode.into())?;
                        if !entry.matching_attributes(&mut matches) {
                            continue;
                        }
                        print_match(&matches, path, &mut out)?;
                    }
                }

                if !pathspec_matched_entry {
                    // TODO(borrowchk): this shouldn't be necessary at all, but `pathspec` stays borrowed mutably for some reason.
                    //                  It's probably due to the strange lifetimes of `index_entries_with_paths()`.
                    let pathspec = repo.pathspec(
                        true,
                        patterns.iter(),
                        true,
                        &index,
                        gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
                            .adjust_for_bare(repo.is_bare()),
                    )?;
                    let workdir = repo.workdir();
                    for pattern in pathspec.search().patterns() {
                        let path = pattern.path();
                        let entry = cache.at_entry(
                            path,
                            Some(is_dir_to_mode(
                                workdir.is_some_and(|wd| wd.join(gix::path::from_bstr(path)).is_dir())
                                    || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR),
                            )),
                        )?;
                        if !entry.matching_attributes(&mut matches) {
                            continue;
                        }
                        print_match(&matches, path, &mut out)?;
                    }
                }
            }
        }

        if let Some(stats) = statistics.then(|| cache.take_statistics()) {
            out.flush()?;
            writeln!(err, "{stats:#?}").ok();
        }
        Ok(())
    }

    fn print_match(
        matches: &gix::attrs::search::Outcome,
        path: &BStr,
        mut out: impl std::io::Write,
    ) -> std::io::Result<()> {
        for m in matches.iter() {
            writeln!(
                out,
                "{}:{}:{}\t{}\t{}",
                m.location.source.map(Path::to_string_lossy).unwrap_or_default(),
                m.location.sequence_number,
                m.pattern,
                path,
                m.assignment
            )?;
        }
        Ok(())
    }
}

pub(crate) fn attributes_cache(
    repo: &gix::Repository,
) -> anyhow::Result<(gix::AttributeStack<'_>, IndexPersistedOrInMemory)> {
    let index = repo.index_or_load_from_head()?;
    let cache = repo.attributes(
        &index,
        if repo.is_bare() {
            gix::worktree::stack::state::attributes::Source::IdMapping
        } else {
            gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
        },
        gix::worktree::stack::state::ignore::Source::IdMapping,
        None,
    )?;
    Ok((cache, index))
}
