package formatter

import (
	"bytes"
	"fmt"
	"strings"

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

// Sarif is an implementation of the Formatter interface
// which formats revive failures into SARIF format.
type Sarif struct {
	Metadata lint.FormatterMetadata
}

// Name returns the name of the formatter
func (*Sarif) Name() string {
	return "sarif"
}

const reviveSite = "https://github.com/mgechev/revive"

// Format formats the failures gotten from the lint.
func (*Sarif) Format(failures <-chan lint.Failure, cfg lint.Config) (string, error) {
	sarifLog := newReviveRunLog(cfg)

	for failure := range failures {
		sarifLog.AddResult(failure)
	}

	buf := new(bytes.Buffer)
	sarifLog.PrettyWrite(buf)

	return buf.String(), nil
}

type reviveRunLog struct {
	*garif.LogFile
	run   *garif.Run
	rules map[string]lint.RuleConfig
}

func newReviveRunLog(cfg lint.Config) *reviveRunLog {
	run := garif.NewRun(garif.NewTool(garif.NewDriver("revive").WithInformationUri(reviveSite)))
	log := garif.NewLogFile([]*garif.Run{run}, garif.Version210)

	reviveLog := &reviveRunLog{
		log,
		run,
		cfg.Rules,
	}

	reviveLog.addRules(cfg.Rules)

	return reviveLog
}

func (l *reviveRunLog) addRules(cfg map[string]lint.RuleConfig) {
	for name, ruleCfg := range cfg {
		rule := garif.NewRule(name).WithHelpUri(reviveSite + "/r#" + name)
		setRuleProperties(rule, ruleCfg)
		driver := l.run.Tool.Driver

		if driver.Rules == nil {
			driver.Rules = []*garif.ReportingDescriptor{rule}
			return
		}

		driver.Rules = append(driver.Rules, rule)
	}
}

func (l *reviveRunLog) AddResult(failure lint.Failure) {
	positiveOrZero := func(x int) int {
		if x > 0 {
			return x
		}
		return 0
	}
	position := failure.Position
	filename := position.Start.Filename
	line := positiveOrZero(position.Start.Line)     // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#def_line
	column := positiveOrZero(position.Start.Column) // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#def_column

	result := garif.NewResult(garif.NewMessageFromText(failure.Failure))
	location := garif.NewLocation().WithURI(filename).WithLineColumn(line, column)
	result.Locations = append(result.Locations, location)
	result.RuleId = failure.RuleName
	result.Level = garif.ResultLevel(l.rules[failure.RuleName].Severity)

	l.run.Results = append(l.run.Results, result)
}

func setRuleProperties(sarifRule *garif.ReportingDescriptor, lintRule lint.RuleConfig) {
	arguments := make([]string, len(lintRule.Arguments))
	for i, arg := range lintRule.Arguments {
		arguments[i] = fmt.Sprintf("%+v", arg)
	}

	if len(arguments) > 0 {
		sarifRule.WithProperties("arguments", strings.Join(arguments, ","))
	}

	sarifRule.WithProperties("severity", string(lintRule.Severity))
}