use super::*;

#[derive(Debug)]
pub(crate) enum Error<'src> {
  AmbiguousModuleFile {
    module: Name<'src>,
    found: Vec<PathBuf>,
  },
  ArgumentPatternMismatch {
    argument: String,
    parameter: &'src str,
    pattern: Box<Pattern<'src>>,
    recipe: &'src str,
  },
  Assert {
    message: String,
    name: Name<'src>,
  },
  Backtick {
    token: Token<'src>,
    output_error: OutputError,
  },
  ChooserInvoke {
    shell_binary: String,
    shell_arguments: String,
    chooser: OsString,
    io_error: io::Error,
  },
  ChooserRead {
    chooser: OsString,
    io_error: io::Error,
  },
  ChooserStatus {
    chooser: OsString,
    status: ExitStatus,
  },
  ChooserWrite {
    chooser: OsString,
    io_error: io::Error,
  },
  CircularImport {
    current: PathBuf,
    import: PathBuf,
  },
  Code {
    recipe: &'src str,
    line_number: Option<usize>,
    code: i32,
    print_message: bool,
  },
  CommandInvoke {
    binary: OsString,
    arguments: Vec<OsString>,
    io_error: io::Error,
  },
  CommandStatus {
    binary: OsString,
    arguments: Vec<OsString>,
    status: ExitStatus,
  },
  Compile {
    compile_error: CompileError<'src>,
  },
  Config {
    config_error: ConfigError,
  },
  Const {
    const_error: ConstError<'src>,
  },
  Cygpath {
    recipe: &'src str,
    output_error: OutputError,
  },
  DefaultRecipeRequiresArguments {
    recipe: &'src str,
    min_arguments: usize,
  },
  Dotenv {
    dotenv_error: dotenvy::Error,
    path: PathBuf,
  },
  DotenvRequired,
  DumpJson {
    source: serde_json::Error,
  },
  DuplicateOption {
    recipe: &'src str,
    option: Switch,
  },
  EditorInvoke {
    editor: OsString,
    io_error: io::Error,
  },
  EditorStatus {
    editor: OsString,
    status: ExitStatus,
  },
  EvalUnknownSubmodule {
    component: String,
    suggestion: Option<Suggestion<'src>>,
  },
  EvalUnknownSubmoduleOrVariable {
    component: String,
    suggestion: Option<Suggestion<'src>>,
  },
  ExcessInvocations {
    invocations: usize,
  },
  ExpectedSubmoduleButFoundRecipe {
    path: String,
  },
  FilesystemIo {
    source: io::Error,
    path: PathBuf,
  },
  FlagWithValue {
    recipe: &'src str,
    option: Switch,
  },
  FormatCheckFoundDiff,
  FunctionCall {
    function: Name<'src>,
    message: String,
  },
  GetConfirmation {
    io_error: io::Error,
  },
  GuardCode {
    recipe: &'src str,
    line_number: usize,
    code: i32,
  },
  Homedir,
  InitExists {
    justfile: PathBuf,
  },
  Internal {
    message: String,
  },
  Interrupted {
    signal: Signal,
  },
  Load {
    path: PathBuf,
    io_error: io::Error,
  },
  MissingImportFile {
    path: Token<'src>,
  },
  MissingModuleFile {
    module: Name<'src>,
  },
  MissingOption {
    recipe: &'src str,
    option: Switch,
  },
  MultipleShortOptions {
    options: String,
  },
  NoChoosableRecipes,
  NoDefaultRecipe,
  NoRecipes,
  NotConfirmed {
    recipe: &'src str,
  },
  OptionMissingValue {
    recipe: &'src str,
    option: Switch,
  },
  PositionalArgumentCountMismatch {
    recipe: Box<Recipe<'src>>,
    found: usize,
    min: usize,
    max: usize,
  },
  RecursionLimit {
    last: Name<'src>,
  },
  RegexCompile {
    source: regex::Error,
  },
  RuntimeDirIo {
    io_error: io::Error,
    path: PathBuf,
  },
  Script {
    command: String,
    io_error: io::Error,
    recipe: &'src str,
  },
  Search {
    search_error: SearchError,
  },
  Shebang {
    argument: Option<String>,
    command: String,
    io_error: io::Error,
    recipe: &'src str,
  },
  ShellIo {
    io_error: io::Error,
    recipe: &'src str,
    shell: String,
  },
  Signal {
    line_number: Option<usize>,
    print_message: bool,
    recipe: &'src str,
    signal: i32,
  },
  #[cfg(windows)]
  SignalHandlerInstall {
    source: ctrlc::Error,
  },
  #[cfg(unix)]
  SignalHandlerPipeCloexec {
    io_error: io::Error,
  },
  #[cfg(unix)]
  SignalHandlerPipeOpen {
    io_error: io::Error,
  },
  #[cfg(unix)]
  SignalHandlerSigaction {
    signal: Signal,
    io_error: io::Error,
  },
  #[cfg(unix)]
  SignalHandlerSpawnThread {
    io_error: io::Error,
  },
  StdoutIo {
    io_error: io::Error,
  },
  TempdirIo {
    recipe: &'src str,
    io_error: io::Error,
  },
  Unknown {
    line_number: Option<usize>,
    print_message: bool,
    recipe: &'src str,
  },
  UnknownGroup {
    group: String,
  },
  UnknownOption {
    recipe: &'src str,
    option: Switch,
  },
  UnknownOverrides {
    overrides: Vec<String>,
  },
  UnknownRecipe {
    recipe: String,
    suggestion: Option<Suggestion<'src>>,
  },
  UnknownSubmodule {
    path: String,
  },
  UnstableFeature {
    unstable_feature: UnstableFeature,
  },
  WriteJustfile {
    justfile: PathBuf,
    io_error: io::Error,
  },
}

impl<'src> Error<'src> {
  pub(crate) fn code(&self) -> Option<i32> {
    match self {
      Self::Backtick {
        output_error: OutputError::Code(code),
        ..
      }
      | Self::Code { code, .. } => Some(*code),

      Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(),
      Self::Backtick {
        output_error: OutputError::Signal(signal),
        ..
      }
      | Self::Signal { signal, .. } => 128i32.checked_add(*signal),
      Self::Backtick {
        output_error: OutputError::Interrupted(signal),
        ..
      }
      | Self::Interrupted { signal } => Some(signal.code()),
      _ => None,
    }
  }

  fn context(&self) -> Option<Token<'src>> {
    match self {
      Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {
        Some(module.token)
      }
      Self::Assert { name, .. } => Some(**name),
      Self::Backtick { token, .. } => Some(*token),
      Self::Compile { compile_error } => Some(compile_error.context()),
      Self::Const { const_error } => Some(const_error.context()),
      Self::FunctionCall { function, .. } => Some(function.token),
      Self::MissingImportFile { path } => Some(*path),
      _ => None,
    }
  }

  /// `Self::Signal` if process was terminated by a signal otherwise
  /// `Self::UnknownFailure`.
  pub(crate) fn from_signal(
    exit_status: ExitStatus,
    line_number: Option<usize>,
    print_message: bool,
    recipe: &'src str,
  ) -> Self {
    match Platform::signal_from_exit_status(exit_status) {
      Some(signal) => Self::Signal {
        line_number,
        print_message,
        recipe,
        signal,
      },
      None => Self::Unknown {
        line_number,
        print_message,
        recipe,
      },
    }
  }

  pub(crate) fn internal(message: impl Into<String>) -> Self {
    Self::Internal {
      message: message.into(),
    }
  }

  pub(crate) fn print_message(&self) -> bool {
    match self {
      Self::Code { print_message, .. }
      | Self::Signal { print_message, .. }
      | Self::Unknown { print_message, .. } => *print_message,
      _ => true,
    }
  }

  fn source(&self) -> Option<&dyn std::error::Error> {
    match self {
      Self::Compile { compile_error } => compile_error.source(),
      _ => None,
    }
  }

  fn suggestion(&self) -> Option<&Suggestion> {
    match self {
      Self::EvalUnknownSubmodule { suggestion, .. }
      | Self::EvalUnknownSubmoduleOrVariable { suggestion, .. }
      | Self::UnknownRecipe { suggestion, .. } => suggestion.as_ref(),
      _ => None,
    }
  }
}

impl<'src> From<CompileError<'src>> for Error<'src> {
  fn from(compile_error: CompileError<'src>) -> Self {
    Self::Compile { compile_error }
  }
}

impl From<ConfigError> for Error<'_> {
  fn from(config_error: ConfigError) -> Self {
    Self::Config { config_error }
  }
}

impl<'src> From<ConstError<'src>> for Error<'src> {
  fn from(const_error: ConstError<'src>) -> Self {
    Self::Const { const_error }
  }
}

impl From<SearchError> for Error<'_> {
  fn from(search_error: SearchError) -> Self {
    Self::Search { search_error }
  }
}

impl ColorDisplay for Error<'_> {
  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
    use Error::*;

    let error = color.error().paint("error");
    let message = color.message().prefix();
    write!(f, "{error}: {message}")?;

    match self {
      AmbiguousModuleFile { module, found } => write!(
        f,
        "found multiple source files for module `{module}`: {}",
        List::and_ticked(found.iter().map(|path| path.display())),
      )?,
      ArgumentPatternMismatch {
        argument,
        parameter,
        pattern,
        recipe,
      } => {
        write!(
          f,
          "argument `{argument}` passed to recipe `{recipe}` parameter `{parameter}` does not match pattern '{}'",
          pattern.original(),
        )?;
      }
      Assert { message, .. } => {
        write!(f, "assert failed: {message}")?;
      }
      Backtick { output_error, .. } => match output_error {
        OutputError::Code(code) => write!(f, "backtick failed with exit code {code}")?,
        OutputError::Signal(signal) => write!(f, "backtick was terminated by signal {signal}")?,
        OutputError::Unknown => write!(f, "backtick failed for an unknown reason")?,
        OutputError::Interrupted(signal) => write!(
          f,
          "backtick succeeded but `just` was interrupted by signal {signal}",
        )?,
        OutputError::Io(io_error) => match io_error.kind() {
          io::ErrorKind::NotFound => write!(
            f,
            "backtick could not be run because just could not find the shell:\n{io_error}",
          ),
          io::ErrorKind::PermissionDenied => write!(
            f,
            "backtick could not be run because just could not run the shell:\n{io_error}",
          ),
          _ => write!(
            f,
            "backtick could not be run because of an IO error while launching the shell:\n{io_error}",
          ),
        }?,
        OutputError::Utf8(utf8_error) => write!(
          f,
          "backtick succeeded but stdout was not utf8: {utf8_error}",
        )?,
      },
      ChooserInvoke {
        shell_binary,
        shell_arguments,
        chooser,
        io_error,
      } => {
        let chooser = chooser.to_string_lossy();
        write!(
          f,
          "chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}",
        )?;
      }
      ChooserRead { chooser, io_error } => {
        let chooser = chooser.to_string_lossy();
        write!(
          f,
          "failed to read output from chooser `{chooser}`: {io_error}",
        )?;
      }
      ChooserStatus { chooser, status } => {
        let chooser = chooser.to_string_lossy();
        write!(f, "chooser `{chooser}` failed: {status}")?;
      }
      ChooserWrite { chooser, io_error } => {
        let chooser = chooser.to_string_lossy();
        write!(f, "failed to write to chooser `{chooser}`: {io_error}")?;
      }
      CircularImport { current, import } => {
        let import = import.display();
        let current = current.display();
        write!(f, "import `{import}` in `{current}` is circular")?;
      }
      Code {
        recipe,
        line_number,
        code,
        ..
      } => {
        if let Some(n) = line_number {
          write!(
            f,
            "recipe `{recipe}` failed on line {n} with exit code {code}",
          )?;
        } else {
          write!(f, "recipe `{recipe}` failed with exit code {code}")?;
        }
      }
      CommandInvoke {
        binary,
        arguments,
        io_error,
      } => {
        let cmd = format_cmd(binary, arguments);
        write!(f, "failed to invoke {cmd}: {io_error}")?;
      }
      CommandStatus {
        binary,
        arguments,
        status,
      } => {
        let cmd = format_cmd(binary, arguments);
        write!(f, "command {cmd} failed: {status}")?;
      }
      Compile { compile_error } => Display::fmt(compile_error, f)?,
      Config { config_error } => Display::fmt(config_error, f)?,
      Const { const_error } => write!(f, "{const_error}")?,
      Cygpath {
        recipe,
        output_error,
      } => match output_error {
        OutputError::Code(code) => write!(
          f,
          "cygpath failed with exit code {code} while translating recipe `{recipe}` shebang interpreter path",
        )?,
        OutputError::Signal(signal) => write!(
          f,
          "cygpath terminated by signal {signal} while translating recipe `{recipe}` shebang interpreter path",
        )?,
        OutputError::Unknown => write!(
          f,
          "cygpath experienced an unknown failure while translating recipe `{recipe}` shebang interpreter path",
        )?,
        OutputError::Interrupted(signal) => write!(
          f,
          "cygpath succeeded but `just` was interrupted by {signal}",
        )?,
        OutputError::Io(io_error) => {
          match io_error.kind() {
            io::ErrorKind::NotFound => write!(
              f,
              "could not find `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}",
            ),
            io::ErrorKind::PermissionDenied => write!(
              f,
              "could not run `cygpath` executable to translate recipe `{recipe}` shebang interpreter path:\n{io_error}",
            ),
            _ => write!(f, "could not run `cygpath` executable:\n{io_error}"),
          }?;
        }
        OutputError::Utf8(utf8_error) => write!(
          f,
          "cygpath successfully translated recipe `{recipe}` shebang interpreter path, but output was not utf8: {utf8_error}",
        )?,
      },
      DefaultRecipeRequiresArguments {
        recipe,
        min_arguments,
      } => {
        let count = Count("argument", *min_arguments);
        write!(
          f,
          "recipe `{recipe}` cannot be used as default recipe since it requires at least {min_arguments} {count}",
        )?;
      }
      Dotenv { dotenv_error, path } => {
        write!(
          f,
          "failed to load environment file from `{}`: {dotenv_error}",
          path.display(),
        )?;
      }
      DotenvRequired => {
        write!(f, "dotenv file not found")?;
      }
      DumpJson { source } => {
        write!(f, "failed to dump JSON to stdout: {source}")?;
      }
      DuplicateOption { recipe, option } => {
        write!(
          f,
          "recipe `{recipe}` option `{option}` cannot be passed more than once",
        )?;
      }
      EditorInvoke { editor, io_error } => {
        let editor = editor.to_string_lossy();
        write!(f, "editor `{editor}` invocation failed: {io_error}")?;
      }
      EditorStatus { editor, status } => {
        let editor = editor.to_string_lossy();
        write!(f, "editor `{editor}` failed: {status}")?;
      }
      EvalUnknownSubmodule { component, .. } => {
        write!(f, "justfile does not contain submodule `{component}`")?;
      }
      EvalUnknownSubmoduleOrVariable { component, .. } => {
        write!(
          f,
          "justfile does not contain variable or submodule `{component}`"
        )?;
      }
      ExcessInvocations { invocations } => {
        write!(
          f,
          "expected 1 command-line recipe invocation but found {invocations}",
        )?;
      }
      ExpectedSubmoduleButFoundRecipe { path } => {
        write!(f, "expected submodule at `{path}` but found recipe")?;
      }
      FilesystemIo { source, path } => {
        write!(f, "I/O error at `{}`: {source}", path.display())?;
      }
      FlagWithValue { recipe, option } => {
        write!(f, "recipe `{recipe}` flag `{option}` does not take value")?;
      }
      FormatCheckFoundDiff => {
        write!(f, "formatted justfile differs from original")?;
      }
      FunctionCall { function, message } => {
        let function = function.lexeme();
        write!(f, "call to function `{function}` failed: {message}")?;
      }
      GetConfirmation { io_error } => {
        write!(f, "failed to read confirmation from stdin: {io_error}")?;
      }
      GuardCode {
        recipe,
        line_number,
        code,
      } => {
        write!(
          f,
          "guard line in recipe `{recipe}` on line {line_number} returned reserved exit code {code}",
        )?;
      }
      Homedir => {
        write!(f, "failed to get homedir")?;
      }
      InitExists { justfile } => {
        write!(f, "justfile `{}` already exists", justfile.display())?;
      }
      Internal { message } => {
        write!(
          f,
          "internal runtime error, this may indicate a bug in just: {message}\n\
          consider filing an issue: https://github.com/casey/just/issues/new",
        )?;
      }
      Interrupted { signal } => {
        write!(f, "interrupted by {signal}")?;
      }
      ShellIo {
        recipe,
        io_error,
        shell,
      } => {
        match io_error.kind() {
          io::ErrorKind::NotFound => write!(
            f,
            "recipe `{recipe}` could not be run because just could not find the shell `{shell}`: \
            {io_error}",
          ),
          io::ErrorKind::PermissionDenied => write!(
            f,
            "recipe `{recipe}` could not be run because just could not run the shell `{shell}`: \
            {io_error}",
          ),
          _ => write!(
            f,
            "recipe `{recipe}` could not be run because of an IO error while launching the shell \
            `{shell}`: {io_error}",
          ),
        }?;
      }
      Load { io_error, path } => {
        write!(
          f,
          "failed to read justfile at `{}`: {io_error}",
          path.display()
        )?;
      }
      MissingImportFile { .. } => write!(f, "could not find source file for import")?,
      MissingModuleFile { module } => {
        write!(f, "could not find source file for module `{module}`")?;
      }
      MissingOption { recipe, option } => {
        write!(f, "recipe `{recipe}` requires option `{option}`")?;
      }
      MultipleShortOptions { options } => {
        write!(
          f,
          "passing multiple short options (`-{options}`) in one argument is not supported"
        )?;
      }
      NoChoosableRecipes => write!(f, "justfile contains no choosable recipes")?,
      NoDefaultRecipe => write!(f, "justfile contains no default recipe")?,
      NoRecipes => write!(f, "justfile contains no recipes")?,
      NotConfirmed { recipe } => {
        write!(f, "recipe `{recipe}` was not confirmed")?;
      }
      OptionMissingValue { recipe, option } => {
        write!(f, "recipe `{recipe}` option `{option}` missing value")?;
      }
      PositionalArgumentCountMismatch {
        recipe,
        found,
        min,
        max,
        ..
      } => {
        let count = Count("argument", *found);
        if min == max {
          let expected = min;
          let only = if expected < found { "only " } else { "" };
          write!(
            f,
            "recipe `{}` got {found} positional {count} but {only}takes {expected}",
            recipe.name(),
          )?;
        } else if found < min {
          write!(
            f,
            "recipe `{}` got {found} positional {count} but takes at least {min}",
            recipe.name(),
          )?;
        } else if found > max {
          write!(
            f,
            "recipe `{}` got {found} positional {count} but takes at most {max}",
            recipe.name(),
          )?;
        }
      }
      RecursionLimit { last } => write!(
        f,
        "maximum recursion depth of {RECURSION_LIMIT} exceeded while calling function {last}"
      )?,
      RegexCompile { source } => write!(f, "{source}")?,
      RuntimeDirIo { io_error, path } => {
        write!(
          f,
          "I/O error in runtime dir `{}`: {io_error}",
          path.display(),
        )?;
      }
      Script {
        command,
        io_error,
        recipe,
      } => {
        write!(
          f,
          "recipe `{recipe}` with command `{command}` execution error: {io_error}",
        )?;
      }
      Search { search_error } => Display::fmt(search_error, f)?,
      Shebang {
        recipe,
        command,
        argument,
        io_error,
      } => {
        if let Some(argument) = argument {
          write!(
            f,
            "recipe `{recipe}` with shebang `#!{command} {argument}` execution error: {io_error}",
          )?;
        } else {
          write!(
            f,
            "recipe `{recipe}` with shebang `#!{command}` execution error: {io_error}",
          )?;
        }
      }
      Signal {
        recipe,
        line_number,
        signal,
        ..
      } => {
        if let Some(n) = line_number {
          write!(
            f,
            "recipe `{recipe}` was terminated on line {n} by signal {signal}",
          )?;
        } else {
          write!(f, "recipe `{recipe}` was terminated by signal {signal}")?;
        }
      }
      #[cfg(windows)]
      SignalHandlerInstall { source } => {
        write!(f, "could not install signal handler: {source}")?;
      }
      #[cfg(unix)]
      SignalHandlerPipeCloexec { io_error } => {
        write!(
          f,
          "I/O error setting O_CLOEXEC on signal handler pipe: {io_error}",
        )?;
      }
      #[cfg(unix)]
      SignalHandlerPipeOpen { io_error } => {
        write!(f, "I/O error opening signal handler pipe: {io_error}")?;
      }
      #[cfg(unix)]
      SignalHandlerSigaction { io_error, signal } => {
        write!(f, "I/O error setting sigaction for {signal}: {io_error}")?;
      }
      #[cfg(unix)]
      SignalHandlerSpawnThread { io_error } => {
        write!(
          f,
          "I/O error spawning thread for signal handler: {io_error}",
        )?;
      }
      StdoutIo { io_error } => {
        write!(f, "I/O error writing to stdout: {io_error}")?;
      }
      TempdirIo { recipe, io_error } => {
        write!(
          f,
          "recipe `{recipe}` could not be run because of an IO error while trying to create a temporary \
          directory or write a file to that directory: {io_error}",
        )?;
      }
      Unknown {
        recipe,
        line_number,
        ..
      } => {
        if let Some(n) = line_number {
          write!(
            f,
            "recipe `{recipe}` failed on line {n} for an unknown reason",
          )?;
        } else {
          write!(f, "recipe `{recipe}` failed for an unknown reason")?;
        }
      }
      UnknownOption { recipe, option } => {
        write!(f, "recipe `{recipe}` does not have option `{option}`")?;
      }
      UnknownOverrides { overrides } => {
        let count = Count("variable", overrides.len());
        let overrides = List::and_ticked(overrides);
        write!(
          f,
          "{count} {overrides} overridden on the command line but not present in justfile",
        )?;
      }
      UnknownGroup { group } => {
        write!(f, "justfile does not contain group `{group}`")?;
      }
      UnknownRecipe { recipe, .. } => {
        write!(f, "justfile does not contain recipe `{recipe}`")?;
      }
      UnknownSubmodule { path } => {
        write!(f, "justfile does not contain submodule `{path}`")?;
      }
      UnstableFeature { unstable_feature } => {
        write!(
          f,
          "{unstable_feature}, invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features",
        )?;
      }
      WriteJustfile { justfile, io_error } => {
        let justfile = justfile.display();
        write!(f, "failed to write justfile to `{justfile}`: {io_error}")?;
      }
    }

    if let Some(suggestion) = self.suggestion() {
      write!(f, "\n{suggestion}")?;
    }

    write!(f, "{}", color.message().suffix())?;

    if let PositionalArgumentCountMismatch { recipe, .. } = self {
      writeln!(f)?;
      let path = Modulepath::try_from([recipe.name()].as_slice()).unwrap();
      write!(
        f,
        "{}",
        Usage {
          long: false,
          path: &path,
          recipe,
        }
        .color_display(color)
      )?;
    }

    if let Some(token) = self.context() {
      writeln!(f)?;
      write!(f, "{}", token.color_display(color.error()))?;
    }

    if let Some(source) = self.source() {
      writeln!(f)?;
      write!(f, "caused by: {source}")?;
    }

    Ok(())
  }
}

fn format_cmd(binary: &OsString, arguments: &Vec<OsString>) -> String {
  iter::once(binary)
    .chain(arguments)
    .map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
    .collect::<Vec<String>>()
    .join(" ")
}
