package golinters

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/pkg/errors"

	"github.com/golangci/go-tools/config"
	"github.com/golangci/go-tools/stylecheck"

	"github.com/golangci/go-tools/lint"
	"github.com/golangci/go-tools/lint/lintutil"
	"github.com/golangci/go-tools/simple"
	"github.com/golangci/go-tools/staticcheck"
	"github.com/golangci/go-tools/unused"
	"golang.org/x/tools/go/packages"

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

const (
	MegacheckParentName      = "megacheck"
	MegacheckStaticcheckName = "staticcheck"
	MegacheckUnusedName      = "unused"
	MegacheckGosimpleName    = "gosimple"
	MegacheckStylecheckName  = "stylecheck"
)

type Staticcheck struct {
	megacheck
}

func NewStaticcheck() *Staticcheck {
	return &Staticcheck{
		megacheck: megacheck{
			staticcheckEnabled: true,
		},
	}
}

func (Staticcheck) Name() string { return MegacheckStaticcheckName }
func (Staticcheck) Desc() string {
	return "Staticcheck is a go vet on steroids, applying a ton of static analysis checks"
}

type Gosimple struct {
	megacheck
}

func NewGosimple() *Gosimple {
	return &Gosimple{
		megacheck: megacheck{
			gosimpleEnabled: true,
		},
	}
}

func (Gosimple) Name() string { return MegacheckGosimpleName }
func (Gosimple) Desc() string {
	return "Linter for Go source code that specializes in simplifying a code"
}

type Unused struct {
	megacheck
}

func NewUnused() *Unused {
	return &Unused{
		megacheck: megacheck{
			unusedEnabled: true,
		},
	}
}

func (Unused) Name() string { return MegacheckUnusedName }
func (Unused) Desc() string {
	return "Checks Go code for unused constants, variables, functions and types"
}

type Stylecheck struct {
	megacheck
}

func NewStylecheck() *Stylecheck {
	return &Stylecheck{
		megacheck: megacheck{
			stylecheckEnabled: true,
		},
	}
}

func (Stylecheck) Name() string { return MegacheckStylecheckName }
func (Stylecheck) Desc() string { return "Stylecheck is a replacement for golint" }

type megacheck struct {
	unusedEnabled      bool
	gosimpleEnabled    bool
	staticcheckEnabled bool
	stylecheckEnabled  bool
}

func (megacheck) Name() string {
	return MegacheckParentName
}

func (megacheck) Desc() string {
	return "" // shouldn't be called
}

func (m *megacheck) enableChildLinter(name string) error {
	switch name {
	case MegacheckStaticcheckName:
		m.staticcheckEnabled = true
	case MegacheckGosimpleName:
		m.gosimpleEnabled = true
	case MegacheckUnusedName:
		m.unusedEnabled = true
	case MegacheckStylecheckName:
		m.stylecheckEnabled = true
	default:
		return fmt.Errorf("invalid child linter name %s for metalinter %s", name, m.Name())
	}

	return nil
}

type MegacheckMetalinter struct{}

func (MegacheckMetalinter) Name() string {
	return MegacheckParentName
}

func (MegacheckMetalinter) BuildLinterConfig(enabledChildren []string) (*linter.Config, error) {
	var m megacheck
	for _, name := range enabledChildren {
		if err := m.enableChildLinter(name); err != nil {
			return nil, err
		}
	}

	// TODO: merge linter.Config and linter.Linter or refactor it in another way
	return &linter.Config{
		Linter:           m,
		EnabledByDefault: false,
		NeedsTypeInfo:    true,
		NeedsSSARepr:     true,
		InPresets:        []string{linter.PresetStyle, linter.PresetBugs, linter.PresetUnused},
		Speed:            1,
		AlternativeNames: nil,
		OriginalURL:      "",
		ParentLinterName: "",
	}, nil
}

func (MegacheckMetalinter) DefaultChildLinterNames() []string {
	// no stylecheck here for backwards compatibility for users who enabled megacheck: don't enable extra
	// linter for them
	return []string{MegacheckStaticcheckName, MegacheckGosimpleName, MegacheckUnusedName}
}

func (m MegacheckMetalinter) AllChildLinterNames() []string {
	return append(m.DefaultChildLinterNames(), MegacheckStylecheckName)
}

func (m MegacheckMetalinter) isValidChild(name string) bool {
	for _, child := range m.AllChildLinterNames() {
		if child == name {
			return true
		}
	}

	return false
}

func (m megacheck) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
	// Use OriginalPackages not Packages because `unused` doesn't work properly
	// when we deduplicate normal and test packages.
	issues, err := m.runMegacheck(lintCtx.OriginalPackages, lintCtx.Settings().Unused.CheckExported)
	if err != nil {
		return nil, errors.Wrap(err, "failed to run megacheck")
	}

	if len(issues) == 0 {
		return nil, nil
	}

	res := make([]result.Issue, 0, len(issues))
	meta := MegacheckMetalinter{}
	for _, i := range issues {
		if !meta.isValidChild(i.Checker) {
			lintCtx.Log.Warnf("Bad megacheck checker name %q", i.Checker)
			continue
		}

		res = append(res, result.Issue{
			Pos: i.Position,
			// TODO: use severity
			Text:       fmt.Sprintf("%s: %s", i.Check, i.Text),
			FromLinter: i.Checker,
		})
	}
	return res, nil
}

func (m megacheck) runMegacheck(workingPkgs []*packages.Package, checkExportedUnused bool) ([]lint.Problem, error) {
	var checkers []lint.Checker

	if m.gosimpleEnabled {
		checkers = append(checkers, simple.NewChecker())
	}
	if m.staticcheckEnabled {
		checkers = append(checkers, staticcheck.NewChecker())
	}
	if m.stylecheckEnabled {
		checkers = append(checkers, stylecheck.NewChecker())
	}
	if m.unusedEnabled {
		uc := unused.NewChecker(unused.CheckAll)
		uc.ConsiderReflection = true
		uc.WholeProgram = checkExportedUnused
		checkers = append(checkers, unused.NewLintChecker(uc))
	}

	if len(checkers) == 0 {
		return nil, nil
	}

	cfg := config.Config{}
	opts := &lintutil.Options{
		// TODO: get current go version, but now it doesn't matter,
		// may be needed after next updates of megacheck
		GoVersion: 12,

		Config: cfg,
		// TODO: support Ignores option
	}

	return runMegacheckCheckers(checkers, workingPkgs, opts)
}

// parseIgnore is a copy from megacheck honnef.co/go/tools/lint/lintutil.parseIgnore
// just to not fork megacheck.
func parseIgnore(s string) ([]lint.Ignore, error) {
	var out []lint.Ignore
	if s == "" {
		return nil, nil
	}
	for _, part := range strings.Fields(s) {
		p := strings.Split(part, ":")
		if len(p) != 2 {
			return nil, errors.New("malformed ignore string")
		}
		path := p[0]
		checks := strings.Split(p[1], ",")
		out = append(out, &lint.GlobIgnore{Pattern: path, Checks: checks})
	}
	return out, nil
}

// runMegacheckCheckers is like megacheck honnef.co/go/tools/lint/lintutil.Lint,
// but takes a list of already-parsed packages instead of a list of
// package-paths to parse.
func runMegacheckCheckers(cs []lint.Checker, workingPkgs []*packages.Package, opt *lintutil.Options) ([]lint.Problem, error) {
	stats := lint.PerfStats{
		CheckerInits: map[string]time.Duration{},
	}

	if opt == nil {
		opt = &lintutil.Options{}
	}
	ignores, err := parseIgnore(opt.Ignores)
	if err != nil {
		return nil, err
	}

	// package-parsing elided here
	stats.PackageLoading = 0

	var problems []lint.Problem
	// populating 'problems' with parser-problems elided here

	if len(workingPkgs) == 0 {
		return problems, nil
	}

	l := &lint.Linter{
		Checkers:      cs,
		Ignores:       ignores,
		GoVersion:     opt.GoVersion,
		ReturnIgnored: opt.ReturnIgnored,
		Config:        opt.Config,

		MaxConcurrentJobs: opt.MaxConcurrentJobs,
		PrintStats:        opt.PrintStats,
	}
	problems = append(problems, l.Lint(workingPkgs, &stats)...)

	return problems, nil
}
