package linter

import (
	"fmt"

	"golang.org/x/tools/go/packages"

	"github.com/golangci/golangci-lint/v2/pkg/config"
)

// LastLinter nolintlint must be last because it looks at the results of all the previous linters for unused nolint directives.
const LastLinter = "nolintlint"

type DeprecationLevel int

const (
	DeprecationNone DeprecationLevel = iota
	DeprecationWarning
	DeprecationError
)

type Deprecation struct {
	Since       string
	Message     string
	Replacement string
	Level       DeprecationLevel
}

type Config struct {
	Linter Linter
	Groups map[string]struct{}

	LoadMode packages.LoadMode

	AlternativeNames []string

	OriginalURL     string // URL of original (not forked) repo, needed for autogenerated README
	Internal        bool   // Internal linters cannot be disabled (ex: typecheck).
	CanAutoFix      bool
	IsSlow          bool
	DoesChangeTypes bool

	Since       string
	Deprecation *Deprecation
}

func NewConfig(linter Linter) *Config {
	lc := &Config{
		Linter: linter,
	}
	return lc.WithLoadFiles()
}

func (lc *Config) WithInternal() *Config {
	lc.Internal = true
	return lc
}

func (lc *Config) ConsiderSlow() *Config {
	lc.IsSlow = true
	return lc
}

func (lc *Config) IsSlowLinter() bool {
	return lc.IsSlow
}

func (lc *Config) WithLoadFiles() *Config {
	lc.LoadMode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedModule
	return lc
}

func (lc *Config) WithLoadForGoAnalysis() *Config {
	lc = lc.WithLoadFiles()
	lc.LoadMode |= packages.NeedImports | packages.NeedDeps | packages.NeedExportFile | packages.NeedTypesSizes
	lc.IsSlow = true
	return lc
}

func (lc *Config) WithGroups(names ...string) *Config {
	if lc.Groups == nil {
		lc.Groups = make(map[string]struct{})
	}

	for _, name := range names {
		lc.Groups[name] = struct{}{}
	}

	return lc
}

func (lc *Config) FromGroup(name string) bool {
	_, ok := lc.Groups[name]
	return ok
}

func (lc *Config) WithURL(url string) *Config {
	lc.OriginalURL = url
	return lc
}

func (lc *Config) WithAlternativeNames(names ...string) *Config {
	lc.AlternativeNames = names
	return lc
}

func (lc *Config) WithAutoFix() *Config {
	lc.CanAutoFix = true
	return lc
}

func (lc *Config) WithChangeTypes() *Config {
	lc.DoesChangeTypes = true
	return lc
}

func (lc *Config) WithSince(version string) *Config {
	lc.Since = version
	return lc
}

func (lc *Config) Deprecated(message, version, replacement string, level DeprecationLevel) *Config {
	lc.Deprecation = &Deprecation{
		Since:       version,
		Message:     message,
		Replacement: replacement,
		Level:       level,
	}
	return lc
}

func (lc *Config) DeprecatedWarning(message, version, replacement string) *Config {
	return lc.Deprecated(message, version, replacement, DeprecationWarning)
}

func (lc *Config) DeprecatedError(message, version, replacement string) *Config {
	return lc.Deprecated(message, version, replacement, DeprecationError)
}

func (lc *Config) IsDeprecated() bool {
	return lc.Deprecation != nil
}

func (lc *Config) AllNames() []string {
	return append([]string{lc.Name()}, lc.AlternativeNames...)
}

func (lc *Config) Name() string {
	return lc.Linter.Name()
}

func (lc *Config) WithNoopFallback(cfg *config.Config, cond func(cfg *config.Config) error) *Config {
	if err := cond(cfg); err != nil {
		lc.Linter = NewNoop(lc.Linter, err.Error())
		lc.LoadMode = 0

		return lc.WithLoadFiles()
	}

	return lc
}

func IsGoLowerThanGo122() func(cfg *config.Config) error {
	return isGoLowerThanGo("1.22")
}

func isGoLowerThanGo(v string) func(cfg *config.Config) error {
	return func(cfg *config.Config) error {
		if cfg == nil || config.IsGoGreaterThanOrEqual(cfg.Run.Go, v) {
			return nil
		}

		return fmt.Errorf("this linter is disabled because the Go version (%s) of your project is lower than Go %s", cfg.Run.Go, v)
	}
}
