use super::*;

pub(crate) struct Completer<'run, 'src> {
  config: Config,
  current: &'run str,
  justfile: Justfile<'src>,
}

impl<'run, 'src> Completer<'run, 'src> {
  fn candidate_recipes(&self) -> Vec<CompletionCandidate> {
    let mut candidates = Vec::new();

    for recipe in self.justfile.public_recipes_recursive(&self.config) {
      let path = recipe.recipe_path().to_string();

      if path.starts_with(self.current) {
        candidates.push(CompletionCandidate::new(path).help(recipe.doc.as_ref().map(Into::into)));
      }
    }

    if self.config.complete_aliases {
      for (alias, modulepath) in self.justfile.public_aliases_recursive(&self.config) {
        let name = modulepath.join(alias.name.lexeme());
        if name.starts_with(self.current) {
          candidates
            .push(CompletionCandidate::new(name).help(alias.target.doc.as_ref().map(Into::into)));
        }
      }
    }

    candidates
  }

  pub(crate) fn complete_argument(current: &OsStr) -> Vec<CompletionCandidate> {
    let loader = Loader::new();

    let Some(completer) = Completer::new(current, &loader) else {
      return Vec::new();
    };

    let mut candidates = completer.candidate_recipes();

    for (name, binding) in &completer.justfile.assignments {
      if !binding.private && name.starts_with(completer.current) {
        candidates.push(CompletionCandidate::new(format!("{name}=")));
      }
    }

    candidates.extend(PathCompleter::any().complete(current));

    candidates
  }

  pub(crate) fn complete_group(current: &OsStr) -> Vec<CompletionCandidate> {
    let loader = Loader::new();

    let Some(completer) = Completer::new(current, &loader) else {
      return Vec::new();
    };

    completer
      .justfile
      .public_groups(&completer.config)
      .into_iter()
      .filter(|group| group.starts_with(completer.current))
      .map(CompletionCandidate::new)
      .collect()
  }

  pub(crate) fn complete_recipe(current: &OsStr) -> Vec<CompletionCandidate> {
    let loader = Loader::new();

    let Some(completer) = Completer::new(current, &loader) else {
      return Vec::new();
    };

    completer.candidate_recipes()
  }

  pub(crate) fn complete_variable(current: &OsStr) -> Vec<CompletionCandidate> {
    let loader = Loader::new();

    let Some(completer) = Completer::new(current, &loader) else {
      return Vec::new();
    };

    completer
      .justfile
      .assignments
      .into_iter()
      .filter(|(name, binding)| !binding.private && name.starts_with(completer.current))
      .map(|(name, _)| CompletionCandidate::new(name))
      .collect()
  }

  fn config() -> Option<Config> {
    let mut args = env::args_os().collect::<Vec<OsString>>();

    args.drain(1..3);

    let matches = Arguments::command()
      .ignore_errors(true)
      .try_get_matches_from(args)
      .ok()?;

    let arguments = Arguments::from_arg_matches(&matches).ok()?;

    Config::from_arguments(arguments).ok()
  }

  fn new(current: &'run OsStr, loader: &'src Loader) -> Option<Self> {
    Self::try_new(current.to_str()?, loader).ok()
  }

  fn try_new(current: &'run str, loader: &'src Loader) -> RunResult<'src, Self> {
    let config = if let Some(config) = Self::config() {
      config
    } else {
      Config::new()?
    };

    let search = Search::search(&config)?;

    let compilation = Compiler::compile(&config, loader, &search.justfile)?;

    Ok(Completer {
      config,
      current,
      justfile: compilation.justfile,
    })
  }
}
