package linter

import (
	"bytes"
	"fmt"

	"go.yaml.in/yaml/v3"
	"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
	ConfigSuggestion func() (string, error)
}

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 string, level DeprecationLevel, opts ...func(*Deprecation)) *Config {
	lc.Deprecation = &Deprecation{
		Since:   version,
		Message: message,
		Level:   level,
	}

	for _, opt := range opts {
		opt(lc.Deprecation)
	}

	return lc
}

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

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

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 Replacement[T any](replacement string, mgr func(T) any, data T) func(*Deprecation) {
	return func(d *Deprecation) {
		if replacement == "" {
			return
		}

		d.Replacement = replacement

		if mgr == nil {
			return
		}

		d.ConfigSuggestion = func() (string, error) {
			buf := bytes.NewBuffer([]byte{})

			encoder := yaml.NewEncoder(buf)
			encoder.SetIndent(2)

			linters := map[string]any{
				"enable": []string{d.Replacement},
			}

			replacementSettings := mgr(data)

			if replacementSettings != nil {
				linters["settings"] = map[string]any{d.Replacement: replacementSettings}
			}

			suggestion := map[string]any{
				"linters": linters,
			}

			err := encoder.Encode(suggestion)
			if err != nil {
				return "", fmt.Errorf("%s: invalid configuration: %w", d.Replacement, err)
			}

			return buf.String(), nil
		}
	}
}

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)
	}
}
