package main

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

	"golang.org/x/tools/go/analysis"
)

const quoteInterpolationAnalyzerName = "string_interpolation_quote"

type quoteInterpolationAnalyzerSettings struct {
	IncludedFunctions []string `mapstructure:"included-functions"`
}

// newQuoteInterpolationAnalyzer returns an analyzer to detect manually quoted string interpolation
// with '%s' and "%s". Quoting this way doesn't escape special characters such as endline and makes
// debugging harder later. We encourage to use %q verb instead.
//
//   - Bad
//     return fmt.Errorf("gl_id='%s' is invalid", glID)
//
//   - Bad
//     fmt.Sprintf("fatal: not a git repository: '%s'", repoPath)
//
//   - Good
//     return fmt.Errorf("gl_id=%q is invalid", glID)
//
//   - Good
//     fmt.Sprintf("fatal: not a git repository: %q", repoPath)
//
// For more information:
// https://gitlab.com/gitlab-org/gitaly/-/blob/master/STYLE.md#use-q-when-interpolating-strings
func newQuoteInterpolationAnalyzer(settings *quoteInterpolationAnalyzerSettings) *analysis.Analyzer {
	return &analysis.Analyzer{
		Name: quoteInterpolationAnalyzerName,
		Doc: `Unless it would lead to incorrect results, always use %q when
		interpolating strings. For more information:
		https://gitlab.com/gitlab-org/gitaly/-/blob/master/STYLE.md#use-q-when-interpolating-strings`,
		Run: runStringInterpolationQuoteAnalyzer(settings.IncludedFunctions),
	}
}

// offendedFormatPattern matches string interpolation having '%s' and "%s" format
var offendedFormatPattern = regexp.MustCompile(`['"]%s['"]`)

func analyzeInterpolation(call *ast.CallExpr, pass *analysis.Pass) {
	if len(call.Args) <= 1 {
		return
	}

	if str, ok := call.Args[0].(*ast.BasicLit); ok && str.Kind == token.STRING {
		value := str.Value
		if strings.HasPrefix(value, `'`) || strings.HasPrefix(value, `"`) {
			value = value[1:]
		}
		if strings.HasSuffix(value, `'`) || strings.HasSuffix(value, `"`) {
			value = value[:len(value)-1]
		}
		for _, index := range offendedFormatPattern.FindAllIndex([]byte(value), -1) {
			start := token.Pos(int(str.Pos()) + index[0] + 1)
			end := token.Pos(int(str.Pos()) + index[1])
			pass.Report(analysis.Diagnostic{
				Pos:            start,
				End:            end,
				Message:        "wrapping %s verb with quotes is not encouraged, please use %q instead",
				SuggestedFixes: nil,
			})
		}
	}
}

func runStringInterpolationQuoteAnalyzer(rules []string) func(*analysis.Pass) (interface{}, error) {
	return func(pass *analysis.Pass) (interface{}, error) {
		matcher := NewMatcher(pass)
		for _, file := range pass.Files {
			ast.Inspect(file, func(n ast.Node) bool {
				if call, ok := n.(*ast.CallExpr); ok {
					if matcher.MatchFunction(call, rules) {
						analyzeInterpolation(call, pass)
					}
				}
				return true
			})
		}
		return nil, nil
	}
}
