package lintersdb

import (
	"fmt"
	"os"
	"slices"
	"sort"

	"golang.org/x/exp/maps"

	"github.com/golangci/golangci-lint/pkg/config"
	"github.com/golangci/golangci-lint/pkg/goanalysis"
	"github.com/golangci/golangci-lint/pkg/lint/linter"
	"github.com/golangci/golangci-lint/pkg/logutils"
)

type Builder interface {
	Build(cfg *config.Config) ([]*linter.Config, error)
}

// Manager is a type of database for all linters (internals or plugins).
// It provides methods to access to the linter sets.
type Manager struct {
	log    logutils.Log
	debugf logutils.DebugFunc

	cfg *config.Config

	linters []*linter.Config

	nameToLCs map[string][]*linter.Config
}

// NewManager creates a new Manager.
// This constructor will call the builders to build and store the linters.
func NewManager(log logutils.Log, cfg *config.Config, builders ...Builder) (*Manager, error) {
	m := &Manager{
		log:       log,
		debugf:    logutils.Debug(logutils.DebugKeyEnabledLinters),
		nameToLCs: make(map[string][]*linter.Config),
	}

	m.cfg = cfg
	if cfg == nil {
		m.cfg = config.NewDefault()
	}

	for _, builder := range builders {
		linters, err := builder.Build(m.cfg)
		if err != nil {
			return nil, fmt.Errorf("build linters: %w", err)
		}

		m.linters = append(m.linters, linters...)
	}

	for _, lc := range m.linters {
		for _, name := range lc.AllNames() {
			m.nameToLCs[name] = append(m.nameToLCs[name], lc)
		}
	}

	err := NewValidator(m).Validate(m.cfg)
	if err != nil {
		return nil, err
	}

	return m, nil
}

func (m *Manager) GetLinterConfigs(name string) []*linter.Config {
	return m.nameToLCs[name]
}

func (m *Manager) GetAllSupportedLinterConfigs() []*linter.Config {
	return m.linters
}

func (m *Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config {
	var ret []*linter.Config
	for _, lc := range m.linters {
		if lc.IsDeprecated() {
			continue
		}

		if slices.Contains(lc.InPresets, p) {
			ret = append(ret, lc)
		}
	}

	return ret
}

func (m *Manager) GetEnabledLintersMap() (map[string]*linter.Config, error) {
	enabledLinters := m.build(m.GetAllEnabledByDefaultLinters())

	if os.Getenv(logutils.EnvTestRun) == "1" {
		m.verbosePrintLintersStatus(enabledLinters)
	}

	return enabledLinters, nil
}

// GetOptimizedLinters returns enabled linters after optimization (merging) of multiple linters into a fewer number of linters.
// E.g. some go/analysis linters can be optimized into one metalinter for data reuse and speed up.
func (m *Manager) GetOptimizedLinters() ([]*linter.Config, error) {
	resultLintersSet := m.build(m.GetAllEnabledByDefaultLinters())
	m.verbosePrintLintersStatus(resultLintersSet)

	m.combineGoAnalysisLinters(resultLintersSet)

	resultLinters := maps.Values(resultLintersSet)

	// Make order of execution of linters (go/analysis metalinter and unused) stable.
	sort.Slice(resultLinters, func(i, j int) bool {
		a, b := resultLinters[i], resultLinters[j]

		if b.Name() == linter.LastLinter {
			return true
		}

		if a.Name() == linter.LastLinter {
			return false
		}

		if a.DoesChangeTypes != b.DoesChangeTypes {
			return b.DoesChangeTypes // move type-changing linters to the end to optimize speed
		}
		return a.Name() < b.Name()
	})

	return resultLinters, nil
}

func (m *Manager) GetAllEnabledByDefaultLinters() []*linter.Config {
	var ret []*linter.Config
	for _, lc := range m.linters {
		if lc.EnabledByDefault {
			ret = append(ret, lc)
		}
	}

	return ret
}

//nolint:gocyclo // the complexity cannot be reduced.
func (m *Manager) build(enabledByDefaultLinters []*linter.Config) map[string]*linter.Config {
	m.debugf("Linters config: %#v", m.cfg.Linters)

	resultLintersSet := map[string]*linter.Config{}
	switch {
	case m.cfg.Linters.DisableAll:
		// no default linters
	case len(m.cfg.Linters.Presets) != 0:
		// imply --disable-all
	case m.cfg.Linters.EnableAll:
		resultLintersSet = linterConfigsToMap(m.linters)
	default:
		resultLintersSet = linterConfigsToMap(enabledByDefaultLinters)
	}

	// --presets can only add linters to default set
	for _, p := range m.cfg.Linters.Presets {
		for _, lc := range m.GetAllLinterConfigsForPreset(p) {
			resultLintersSet[lc.Name()] = lc
		}
	}

	// --fast removes slow linters from current set.
	// It should be after --presets to be able to run only fast linters in preset.
	// It should be before --enable and --disable to be able to enable or disable specific linter.
	if m.cfg.Linters.Fast {
		for name, lc := range resultLintersSet {
			if lc.IsSlowLinter() {
				delete(resultLintersSet, name)
			}
		}
	}

	for _, name := range m.cfg.Linters.Enable {
		for _, lc := range m.GetLinterConfigs(name) {
			// it's important to use lc.Name() nor name because name can be alias
			resultLintersSet[lc.Name()] = lc
		}
	}

	for _, name := range m.cfg.Linters.Disable {
		for _, lc := range m.GetLinterConfigs(name) {
			// it's important to use lc.Name() nor name because name can be alias
			delete(resultLintersSet, lc.Name())
		}
	}

	// typecheck is not a real linter and cannot be disabled.
	if _, ok := resultLintersSet["typecheck"]; !ok && (m.cfg == nil || !m.cfg.InternalCmdTest) {
		for _, lc := range m.GetLinterConfigs("typecheck") {
			// it's important to use lc.Name() nor name because name can be alias
			resultLintersSet[lc.Name()] = lc
		}
	}

	return resultLintersSet
}

func (m *Manager) combineGoAnalysisLinters(linters map[string]*linter.Config) {
	mlConfig := &linter.Config{}

	var goanalysisLinters []*goanalysis.Linter

	for _, lc := range linters {
		lnt, ok := lc.Linter.(*goanalysis.Linter)
		if !ok {
			continue
		}

		if lnt.LoadMode() == goanalysis.LoadModeWholeProgram {
			// It's ineffective by CPU and memory to run whole-program and incremental analyzers at once.
			continue
		}

		mlConfig.LoadMode |= lc.LoadMode

		if lc.IsSlowLinter() {
			mlConfig.ConsiderSlow()
		}

		mlConfig.InPresets = append(mlConfig.InPresets, lc.InPresets...)

		goanalysisLinters = append(goanalysisLinters, lnt)
	}

	if len(goanalysisLinters) <= 1 {
		m.debugf("Didn't combine go/analysis linters: got only %d linters", len(goanalysisLinters))
		return
	}

	for _, lnt := range goanalysisLinters {
		delete(linters, lnt.Name())
	}

	// Make order of execution of go/analysis analyzers stable.
	sort.Slice(goanalysisLinters, func(i, j int) bool {
		a, b := goanalysisLinters[i], goanalysisLinters[j]

		if b.Name() == linter.LastLinter {
			return true
		}

		if a.Name() == linter.LastLinter {
			return false
		}

		return a.Name() <= b.Name()
	})

	mlConfig.Linter = goanalysis.NewMetaLinter(goanalysisLinters)

	sort.Strings(mlConfig.InPresets)
	mlConfig.InPresets = slices.Compact(mlConfig.InPresets)

	linters[mlConfig.Linter.Name()] = mlConfig

	m.debugf("Combined %d go/analysis linters into one metalinter", len(goanalysisLinters))
}

func (m *Manager) verbosePrintLintersStatus(lcs map[string]*linter.Config) {
	var linterNames []string
	for _, lc := range lcs {
		if lc.Internal {
			continue
		}

		linterNames = append(linterNames, lc.Name())
	}
	sort.Strings(linterNames)
	m.log.Infof("Active %d linters: %s", len(linterNames), linterNames)

	if len(m.cfg.Linters.Presets) != 0 {
		sort.Strings(m.cfg.Linters.Presets)
		m.log.Infof("Active presets: %s", m.cfg.Linters.Presets)
	}
}

func AllPresets() []string {
	return []string{
		linter.PresetBugs,
		linter.PresetComment,
		linter.PresetComplexity,
		linter.PresetError,
		linter.PresetFormatting,
		linter.PresetImport,
		linter.PresetMetaLinter,
		linter.PresetModule,
		linter.PresetPerformance,
		linter.PresetSQL,
		linter.PresetStyle,
		linter.PresetTest,
		linter.PresetUnused,
	}
}

func linterConfigsToMap(lcs []*linter.Config) map[string]*linter.Config {
	ret := map[string]*linter.Config{}
	for _, lc := range lcs {
		if lc.IsDeprecated() && lc.Deprecation.Level > linter.DeprecationWarning {
			continue
		}

		ret[lc.Name()] = lc
	}

	return ret
}