package processors

import (
	"go/ast"
	"go/parser"
	"go/token"
	"strings"

	"github.com/golangci/golangci-lint/pkg/result"
	"golang.org/x/tools/go/loader"
)

type NolintProcessor struct {
	prog *loader.Program
}

func NewNolintProcessor(prog *loader.Program) *NolintProcessor {
	return &NolintProcessor{
		prog: prog,
	}
}

var _ Processor = NolintProcessor{}

type comment struct {
	linters []string
	line    int
}
type fileComments []comment
type commentsCache map[string]fileComments

func (p NolintProcessor) Name() string {
	return "nolint"
}

func (p NolintProcessor) Process(results []result.Result) ([]result.Result, error) {
	var retResults []result.Result
	parsedFilesCache := commentsCache{}
	for _, res := range results {
		pr, err := p.processResult(res, parsedFilesCache)
		if err != nil {
			return nil, err
		}

		retResults = append(retResults, *pr)
	}

	return retResults, nil
}

func (p NolintProcessor) processResult(r result.Result, parsedFilesCache commentsCache) (*result.Result, error) {
	ret := r
	ret.Issues = nil
	for _, i := range r.Issues {
		skip, err := p.shouldSkipIssue(&i, parsedFilesCache)
		if err != nil {
			return nil, err
		}
		if !skip {
			ret.Issues = append(ret.Issues, i)
		}
	}

	return &ret, nil
}

func (p NolintProcessor) shouldSkipIssue(i *result.Issue, parsedFilesCache commentsCache) (bool, error) {
	comments := parsedFilesCache[i.File]
	if comments == nil {
		file, err := parser.ParseFile(p.prog.Fset, i.File, nil, parser.ParseComments)
		if err != nil {
			return false, err
		}

		comments = extractFileComments(p.prog.Fset, file.Comments...)
		parsedFilesCache[i.File] = comments
	}

	for _, comment := range comments {
		if comment.line != i.LineNumber {
			continue
		}

		if len(comment.linters) == 0 {
			return true, nil // skip all linters
		}

		for _, linter := range comment.linters {
			if i.FromLinter == linter {
				return true, nil
			}
			// TODO: check linter name
		}
	}

	return false, nil
}

func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fileComments {
	ret := fileComments{}
	for _, g := range comments {
		for _, c := range g.List {
			text := strings.TrimLeft(c.Text, "/ ")
			if strings.HasPrefix(text, "nolint") {
				var linters []string
				if strings.HasPrefix(text, "nolint:") {
					text = strings.Split(text, " ")[0] // allow arbitrary text after this comment
					for _, linter := range strings.Split(strings.TrimPrefix(text, "nolint:"), ",") {
						linters = append(linters, strings.TrimSpace(linter))
					}
				}
				pos := fset.Position(g.Pos())
				ret = append(ret, comment{
					linters: linters,
					line:    pos.Line,
				})
			}
		}
	}

	return ret
}