package rule

import (
	"bufio"
	"bytes"
	"fmt"
	"go/ast"
	"go/token"
	"strings"

	"github.com/mgechev/revive/lint"
)

// FileLengthLimitRule lints the number of lines in a file.
type FileLengthLimitRule struct {
	// max is the maximum number of lines allowed in a file. 0 means the rule is disabled.
	max int
	// skipComments indicates whether to skip comment lines when counting lines.
	skipComments bool
	// skipBlankLines indicates whether to skip blank lines when counting lines.
	skipBlankLines bool
}

// Apply applies the rule to given file.
func (r *FileLengthLimitRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
	if r.max <= 0 {
		// when max is negative or 0 the rule is disabled
		return nil
	}

	all := 0
	blank := 0
	scanner := bufio.NewScanner(bytes.NewReader(file.Content()))
	for scanner.Scan() {
		all++
		if len(bytes.TrimSpace(scanner.Bytes())) == 0 {
			blank++
		}
	}

	if err := scanner.Err(); err != nil {
		return newInternalFailureError(err)
	}

	lines := all
	if r.skipComments {
		lines -= countCommentLines(file.AST.Comments)
	}

	if r.skipBlankLines {
		lines -= blank
	}

	if lines <= r.max {
		return nil
	}

	return []lint.Failure{
		{
			Category:   lint.FailureCategoryCodeStyle,
			Confidence: 1,
			Position: lint.FailurePosition{
				Start: token.Position{
					Filename: file.Name,
					Line:     all,
				},
			},
			Failure: fmt.Sprintf("file length is %d lines, which exceeds the limit of %d", lines, r.max),
		},
	}
}

// Configure validates the rule configuration, and configures the rule accordingly.
//
// Configuration implements the [lint.ConfigurableRule] interface.
func (r *FileLengthLimitRule) Configure(arguments lint.Arguments) error {
	if len(arguments) < 1 {
		return nil // use default
	}

	argKV, ok := arguments[0].(map[string]any)
	if !ok {
		return fmt.Errorf(`invalid argument to the "file-length-limit" rule. Expecting a k,v map, got %T`, arguments[0])
	}
	for k, v := range argKV {
		switch {
		case isRuleOption(k, "max"):
			maxLines, ok := v.(int64)
			if !ok || maxLines < 0 {
				return fmt.Errorf(`invalid configuration value for max lines in "file-length-limit" rule; need positive int64 but got %T`, v)
			}
			r.max = int(maxLines)
		case isRuleOption(k, "skipComments"):
			skipComments, ok := v.(bool)
			if !ok {
				return fmt.Errorf(`invalid configuration value for skip comments in "file-length-limit" rule; need bool but got %T`, v)
			}
			r.skipComments = skipComments
		case isRuleOption(k, "skipBlankLines"):
			skipBlankLines, ok := v.(bool)
			if !ok {
				return fmt.Errorf(`invalid configuration value for skip blank lines in "file-length-limit" rule; need bool but got %T`, v)
			}
			r.skipBlankLines = skipBlankLines
		}
	}
	return nil
}

// Name returns the rule name.
func (*FileLengthLimitRule) Name() string {
	return "file-length-limit"
}

func countCommentLines(comments []*ast.CommentGroup) int {
	count := 0
	for _, cg := range comments {
		for _, comment := range cg.List {
			if len(comment.Text) < 2 {
				continue
			}
			switch comment.Text[1] {
			case '/': // single-line comment
				count++
			case '*': // multi-line comment
				count += strings.Count(comment.Text, "\n") + 1
			}
		}
	}
	return count
}
