package processors

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

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

// The values must be in lowercase.
const (
	genCodeGenerated = "code generated"
	genDoNotEdit     = "do not edit"

	// Related to easyjson.
	genAutoFile = "autogenerated file"

	//nolint:lll // Long URL
	// Related to Swagger Codegen.
	// https://github.com/swagger-api/swagger-codegen/blob/61cfeac3b9d855b4eb8bffa0d118bece117bcb7d/modules/swagger-codegen/src/main/resources/go/partial_header.mustache#L16
	// https://github.com/swagger-api/swagger-codegen/issues/12358
	genSwaggerCodegen = "* generated by: swagger codegen "
)

// GeneratedFileMatcher detects generated files.
//   - mode "lax": see `isGeneratedFileLax` documentation.
//   - mode "strict": see `isGeneratedFileStrict` documentation.
//   - mode "disable": skips this processor.
type GeneratedFileMatcher struct {
	debugf logutils.DebugFunc

	mode string
}

func NewGeneratedFileMatcher(mode string) *GeneratedFileMatcher {
	return &GeneratedFileMatcher{
		debugf: logutils.Debug(logutils.DebugKeyGeneratedFileFilter),
		mode:   mode,
	}
}

func (p *GeneratedFileMatcher) IsGeneratedFile(filepath string, src any) (bool, error) {
	if p.mode == config.GeneratedModeDisable {
		return false, nil
	}

	file, err := parser.ParseFile(token.NewFileSet(), filepath, src, parser.PackageClauseOnly|parser.ParseComments)
	if err != nil {
		return false, fmt.Errorf("failed to parse file: %w", err)
	}

	if p.mode == config.GeneratedModeStrict {
		return isGeneratedFileStrict(file), nil
	}

	doc := getComments(file)

	return p.isGeneratedFileLax(doc), nil
}

// isGeneratedFileLax reports whether the source file is generated code.
// The function uses a bit laxer rules than isGeneratedFileStrict to match more generated code.
// See https://github.com/golangci/golangci-lint/issues/48 and https://github.com/golangci/golangci-lint/issues/72.
func (p *GeneratedFileMatcher) isGeneratedFileLax(doc string) bool {
	markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile, genSwaggerCodegen}

	doc = strings.ToLower(doc)

	for _, marker := range markers {
		if strings.Contains(doc, marker) {
			p.debugf("doc contains marker %q: file is generated", marker)

			return true
		}
	}

	p.debugf("doc of len %d doesn't contain any of markers: %s", len(doc), markers)

	return false
}

// isGeneratedFileStrict returns true if the source file has a line that matches the regular expression:
//
//	^// Code generated .* DO NOT EDIT\.$
//
// This line must appear before the first non-comment, non-blank text in the file.
// Based on https://go.dev/s/generatedcode.
func isGeneratedFileStrict(file *ast.File) bool {
	if file == nil || len(file.Comments) == 0 {
		return false
	}

	return ast.IsGenerated(file)
}

func getComments(file *ast.File) string {
	var docLines []string
	for _, c := range file.Comments {
		docLines = append(docLines, strings.TrimSpace(c.Text()))
	}

	return strings.Join(docLines, "\n")
}
