use std::path::Path;

use crate::{
    errors::HgError,
    exit_codes,
    filepatterns::parse_pattern_file_contents,
    matchers::{
        AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
        NeverMatcher,
    },
    repo::Repo,
    requirements::NARROW_REQUIREMENT,
    sparse::{self, SparseConfigError, SparseWarning},
};

/// The file in .hg/store/ that indicates which paths exit in the store
const FILENAME: &str = "narrowspec";
/// The file in .hg/ that indicates which paths exit in the dirstate
const DIRSTATE_FILENAME: &str = "narrowspec.dirstate";

/// Pattern prefixes that are allowed in narrow patterns. This list MUST
/// only contain patterns that are fast and safe to evaluate. Keep in mind
/// that patterns are supplied by clients and executed on remote servers
/// as part of wire protocol commands. That means that changes to this
/// data structure influence the wire protocol and should not be taken
/// lightly - especially removals.
const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"];

/// Return the matcher for the current narrow spec, and all configuration
/// warnings to display.
pub fn matcher(
    repo: &Repo,
) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
    let mut warnings = vec![];
    if !repo.requirements().contains(NARROW_REQUIREMENT) {
        return Ok((Box::new(AlwaysMatcher), warnings));
    }
    // Treat "narrowspec does not exist" the same as "narrowspec file exists
    // and is empty".
    let store_spec = repo.store_vfs().try_read(FILENAME)?.unwrap_or_default();
    let working_copy_spec = repo
        .hg_vfs()
        .try_read(DIRSTATE_FILENAME)?
        .unwrap_or_default();
    if store_spec != working_copy_spec {
        return Err(HgError::abort(
            "abort: working copy's narrowspec is stale",
            exit_codes::STATE_ERROR,
            Some("run 'hg tracked --update-working-copy'".into()),
        )
        .into());
    }

    let config = sparse::parse_config(
        &store_spec,
        sparse::SparseConfigContext::Narrow,
    )?;

    warnings.extend(config.warnings);

    if !config.profiles.is_empty() {
        // TODO (from Python impl) maybe do something with profiles?
        return Err(SparseConfigError::IncludesInNarrow);
    }
    validate_patterns(&config.includes)?;
    validate_patterns(&config.excludes)?;

    if config.includes.is_empty() {
        return Ok((Box::new(NeverMatcher), warnings));
    }

    let (patterns, subwarnings) = parse_pattern_file_contents(
        &config.includes,
        Path::new(""),
        None,
        false,
        true,
    )?;
    warnings.extend(subwarnings.into_iter().map(From::from));

    let mut m: Box<dyn Matcher + Sync> =
        Box::new(IncludeMatcher::new(patterns)?);

    let (patterns, subwarnings) = parse_pattern_file_contents(
        &config.excludes,
        Path::new(""),
        None,
        false,
        true,
    )?;
    if !patterns.is_empty() {
        warnings.extend(subwarnings.into_iter().map(From::from));
        let exclude_matcher = Box::new(IncludeMatcher::new(patterns)?);
        m = Box::new(DifferenceMatcher::new(m, exclude_matcher));
    }

    Ok((m, warnings))
}

fn validate_patterns(patterns: &[u8]) -> Result<(), SparseConfigError> {
    for pattern in patterns.split(|c| *c == b'\n') {
        if pattern.is_empty() {
            continue;
        }
        for prefix in VALID_PREFIXES.iter() {
            if pattern.starts_with(prefix.as_bytes()) {
                return Ok(());
            }
        }
        return Err(SparseConfigError::InvalidNarrowPrefix(
            pattern.to_owned(),
        ));
    }
    Ok(())
}
