package discovery

import (
	"fmt"
	"strings"

	"github.com/gruntwork-io/terragrunt/internal/component"
	"github.com/gruntwork-io/terragrunt/internal/errors"
)

// GitFilterCommandError represents an error that occurs when attempting to use
// Git-based filtering with an unsupported command.
type GitFilterCommandError struct {
	Cmd  string
	Args []string
}

func (e GitFilterCommandError) Error() string {
	command := strings.TrimSpace(
		strings.Join(
			append(
				[]string{e.Cmd},
				e.Args...,
			),
			" ",
		),
	)

	return fmt.Sprintf(
		"Git-based filtering is not supported with the command '%s'. "+
			"Git-based filtering can only be used with 'plan', 'apply', "+
			"or discovery commands (like 'find' or 'list') that don't require additional arguments.",
		command,
	)
}

// NewGitFilterCommandError creates a new GitFilterCommandError with the given command and arguments.
func NewGitFilterCommandError(cmd string, args []string) error {
	return errors.New(GitFilterCommandError{
		Cmd:  cmd,
		Args: args,
	})
}

// MissingDiscoveryContextError represents an error that occurs when a component
// is missing its discovery context during dependency discovery. This indicates
// a bug in Terragrunt.
type MissingDiscoveryContextError struct {
	ComponentPath string
}

func (e MissingDiscoveryContextError) Error() string {
	return fmt.Sprintf(
		"Component at path '%s' is missing its discovery context during dependency discovery. "+
			"This is a bug in Terragrunt. "+
			"Please open a bug report at https://github.com/gruntwork-io/terragrunt/issues "+
			"with details about how you encountered this error.",
		e.ComponentPath,
	)
}

// NewMissingDiscoveryContextError creates a new MissingDiscoveryContextError for the given component path.
func NewMissingDiscoveryContextError(componentPath string) error {
	return errors.New(MissingDiscoveryContextError{
		ComponentPath: componentPath,
	})
}

// MissingWorkingDirectoryError represents an error that occurs when a component's
// discovery context is missing its working directory during dependency discovery.
// This indicates a bug in Terragrunt.
type MissingWorkingDirectoryError struct {
	ComponentPath string
}

func (e MissingWorkingDirectoryError) Error() string {
	return fmt.Sprintf(
		"Component at path '%s' has a discovery context but is missing its working directory during dependency discovery. "+
			"This is a bug in Terragrunt. "+
			"Please open a bug report at https://github.com/gruntwork-io/terragrunt/issues "+
			"with details about how you encountered this error.",
		e.ComponentPath,
	)
}

// NewMissingWorkingDirectoryError creates a new MissingWorkingDirectoryError for the given component path.
func NewMissingWorkingDirectoryError(componentPath string) error {
	return errors.New(MissingWorkingDirectoryError{
		ComponentPath: componentPath,
	})
}

// ClassificationError represents an error during component classification.
type ClassificationError struct {
	ComponentPath string
	Reason        string
}

func (e ClassificationError) Error() string {
	return fmt.Sprintf(
		"Failed to classify component at '%s': %s",
		e.ComponentPath, e.Reason,
	)
}

// NewClassificationError creates a new ClassificationError.
func NewClassificationError(componentPath, reason string) error {
	return errors.New(ClassificationError{
		ComponentPath: componentPath,
		Reason:        reason,
	})
}

// CoexistenceError represents an error when a directory contains both
// a unit configuration file and a stack configuration file.
type CoexistenceError struct {
	ComponentPath string
	ConfigFileA   string
	ConfigFileB   string
}

func (e CoexistenceError) Error() string {
	return fmt.Sprintf(
		"Component %q contains both configuration files %s and %s. "+
			"A component must be either a unit or a stack, not both.",
		e.ComponentPath, e.ConfigFileA, e.ConfigFileB,
	)
}

// NewCoexistenceError creates a new CoexistenceError from two components with different kinds.
func NewCoexistenceError(a, b component.Component) error {
	return errors.New(CoexistenceError{
		ComponentPath: a.Path(),
		ConfigFileA:   a.ConfigFile(),
		ConfigFileB:   b.ConfigFile(),
	})
}

// StackDependencyExpansionError indicates that a stack dependency path could not be expanded into
// its constituent unit paths. Wraps the underlying parse error so callers can extract typed details
// via errors.As.
type StackDependencyExpansionError struct {
	Wrapped error
	DepPath string
}

func (e StackDependencyExpansionError) Error() string {
	return fmt.Sprintf("failed to expand stack dependency path %s: %s", e.DepPath, e.Wrapped)
}

func (e StackDependencyExpansionError) Unwrap() error {
	return e.Wrapped
}

// NewStackDependencyExpansionError wraps err with the dependency path that triggered the expansion.
func NewStackDependencyExpansionError(depPath string, err error) error {
	return errors.New(StackDependencyExpansionError{DepPath: depPath, Wrapped: err})
}
