package goanalysis

import (
	"context"
	"flag"
	"fmt"
	"strings"

	"github.com/pkg/errors"
	"golang.org/x/tools/go/analysis"

	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/checker"
	"github.com/golangci/golangci-lint/pkg/lint/linter"
	"github.com/golangci/golangci-lint/pkg/result"
)

type Linter struct {
	name, desc string
	analyzers  []*analysis.Analyzer
	cfg        map[string]map[string]interface{}
}

func NewLinter(name, desc string, analyzers []*analysis.Analyzer, cfg map[string]map[string]interface{}) *Linter {
	return &Linter{name: name, desc: desc, analyzers: analyzers, cfg: cfg}
}

func (lnt Linter) Name() string {
	return lnt.name
}

func (lnt Linter) Desc() string {
	return lnt.desc
}

func (lnt Linter) allAnalyzerNames() []string {
	var ret []string
	for _, a := range lnt.analyzers {
		ret = append(ret, a.Name)
	}
	return ret
}

func allFlagNames(fs *flag.FlagSet) []string {
	var ret []string
	fs.VisitAll(func(f *flag.Flag) {
		ret = append(ret, f.Name)
	})
	return ret
}

func valueToString(v interface{}) string {
	if ss, ok := v.([]string); ok {
		return strings.Join(ss, ",")
	}

	if is, ok := v.([]interface{}); ok {
		var ss []string
		for _, i := range is {
			ss = append(ss, fmt.Sprint(i))
		}
		return valueToString(ss)
	}

	return fmt.Sprint(v)
}

func (lnt Linter) configureAnalyzer(a *analysis.Analyzer, cfg map[string]interface{}) error {
	for k, v := range cfg {
		f := a.Flags.Lookup(k)
		if f == nil {
			validFlagNames := allFlagNames(&a.Flags)
			if len(validFlagNames) == 0 {
				return fmt.Errorf("analyzer doesn't have settings")
			}

			return fmt.Errorf("analyzer doesn't have setting %q, valid settings: %v",
				k, validFlagNames)
		}

		if err := f.Value.Set(valueToString(v)); err != nil {
			return errors.Wrapf(err, "failed to set analyzer setting %q with value %v", k, v)
		}
	}

	return nil
}

func (lnt Linter) configure() error {
	analyzersMap := map[string]*analysis.Analyzer{}
	for _, a := range lnt.analyzers {
		analyzersMap[a.Name] = a
	}

	for analyzerName, analyzerSettings := range lnt.cfg {
		a := analyzersMap[analyzerName]
		if a == nil {
			return fmt.Errorf("settings key %q must be valid analyzer name, valid analyzers: %v",
				analyzerName, lnt.allAnalyzerNames())
		}

		if err := lnt.configureAnalyzer(a, analyzerSettings); err != nil {
			return errors.Wrapf(err, "failed to configure analyzer %s", analyzerName)
		}
	}

	return nil
}

func (lnt Linter) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
	if err := analysis.Validate(lnt.analyzers); err != nil {
		return nil, errors.Wrap(err, "failed to validate analyzers")
	}

	if err := lnt.configure(); err != nil {
		return nil, errors.Wrap(err, "failed to configure analyzers")
	}

	diags, errs := checker.Run(lnt.analyzers, lintCtx.Packages)
	for i := 1; i < len(errs); i++ {
		lintCtx.Log.Warnf("%s error: %s", lnt.Name(), errs[i])
	}
	if len(errs) != 0 {
		return nil, errs[0]
	}

	var issues []result.Issue
	for _, diag := range diags {
		i := result.Issue{
			FromLinter: lnt.Name(),
			Text:       fmt.Sprintf("%s: %s", diag.AnalyzerName, diag.Message),
			Pos:        diag.Position,
		}
		issues = append(issues, i)
	}

	return issues, nil
}