package sarif

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/google/uuid"
	"github.com/securego/gosec/v2"
	"github.com/securego/gosec/v2/cwe"
)

// GenerateReport Convert a gosec report to a Sarif Report
func GenerateReport(rootPaths []string, data *gosec.ReportInfo) (*Report, error) {
	type rule struct {
		index int
		rule  *ReportingDescriptor
	}

	rules := make([]*ReportingDescriptor, 0)
	rulesIndices := make(map[string]rule)
	lastRuleIndex := -1

	results := []*Result{}
	cweTaxa := make([]*ReportingDescriptor, 0)
	weaknesses := make(map[string]*cwe.Weakness)

	for _, issue := range data.Issues {
		_, ok := weaknesses[issue.Cwe.ID]
		if !ok {
			weakness := cwe.Get(issue.Cwe.ID)
			weaknesses[issue.Cwe.ID] = weakness
			cweTaxon := parseSarifTaxon(weakness)
			cweTaxa = append(cweTaxa, cweTaxon)
		}

		r, ok := rulesIndices[issue.RuleID]
		if !ok {
			lastRuleIndex++
			r = rule{index: lastRuleIndex, rule: parseSarifRule(issue)}
			rulesIndices[issue.RuleID] = r
			rules = append(rules, r.rule)
		}

		location, err := parseSarifLocation(issue, rootPaths)
		if err != nil {
			return nil, err
		}

		result := NewResult(r.rule.ID, r.index, getSarifLevel(issue.Severity.String()), issue.What).
			WithLocations(location)

		results = append(results, result)
	}

	sort.SliceStable(rules, func(i, j int) bool { return rules[i].ID < rules[j].ID })
	sort.SliceStable(cweTaxa, func(i, j int) bool { return cweTaxa[i].ID < cweTaxa[j].ID })

	tool := NewTool(buildSarifDriver(rules, data.GosecVersion))

	cweTaxonomy := buildCWETaxonomy(cweTaxa)

	run := NewRun(tool).
		WithTaxonomies(cweTaxonomy).
		WithResults(results...)

	return NewReport(Version, Schema).
		WithRuns(run), nil
}

// parseSarifRule return SARIF rule field struct
func parseSarifRule(issue *gosec.Issue) *ReportingDescriptor {
	return &ReportingDescriptor{
		ID:               issue.RuleID,
		Name:             issue.What,
		ShortDescription: NewMultiformatMessageString(issue.What),
		FullDescription:  NewMultiformatMessageString(issue.What),
		Help: NewMultiformatMessageString(fmt.Sprintf("%s\nSeverity: %s\nConfidence: %s\n",
			issue.What, issue.Severity.String(), issue.Confidence.String())),
		Properties: &PropertyBag{
			"tags":      []string{"security", issue.Severity.String()},
			"precision": strings.ToLower(issue.Confidence.String()),
		},
		DefaultConfiguration: &ReportingConfiguration{
			Level: getSarifLevel(issue.Severity.String()),
		},
		Relationships: []*ReportingDescriptorRelationship{
			buildSarifReportingDescriptorRelationship(issue.Cwe),
		},
	}
}

func buildSarifReportingDescriptorRelationship(weakness *cwe.Weakness) *ReportingDescriptorRelationship {
	return &ReportingDescriptorRelationship{
		Target: &ReportingDescriptorReference{
			ID:            weakness.ID,
			GUID:          uuid3(weakness.SprintID()),
			ToolComponent: NewToolComponentReference(cwe.Acronym),
		},
		Kinds: []string{"superset"},
	}
}

func buildCWETaxonomy(taxa []*ReportingDescriptor) *ToolComponent {
	return NewToolComponent(cwe.Acronym, cwe.Version, cwe.InformationURI).
		WithReleaseDateUtc(cwe.ReleaseDateUtc).
		WithDownloadURI(cwe.DownloadURI).
		WithOrganization(cwe.Organization).
		WithShortDescription(NewMultiformatMessageString(cwe.Description)).
		WithIsComprehensive(true).
		WithLanguage("en").
		WithMinimumRequiredLocalizedDataSemanticVersion(cwe.Version).
		WithTaxa(taxa...)
}

func parseSarifTaxon(weakness *cwe.Weakness) *ReportingDescriptor {
	return &ReportingDescriptor{
		ID:               weakness.ID,
		GUID:             uuid3(weakness.SprintID()),
		HelpURI:          weakness.SprintURL(),
		FullDescription:  NewMultiformatMessageString(weakness.Description),
		ShortDescription: NewMultiformatMessageString(weakness.Name),
	}
}

func parseSemanticVersion(version string) string {
	if len(version) == 0 {
		return "devel"
	}
	if strings.HasPrefix(version, "v") {
		return version[1:]
	}
	return version
}

func buildSarifDriver(rules []*ReportingDescriptor, gosecVersion string) *ToolComponent {
	semanticVersion := parseSemanticVersion(gosecVersion)
	return NewToolComponent("gosec", gosecVersion, "https://github.com/securego/gosec/").
		WithSemanticVersion(semanticVersion).
		WithSupportedTaxonomies(NewToolComponentReference(cwe.Acronym)).
		WithRules(rules...)
}

func uuid3(value string) string {
	return uuid.NewMD5(uuid.Nil, []byte(value)).String()
}

// parseSarifLocation return SARIF location struct
func parseSarifLocation(issue *gosec.Issue, rootPaths []string) (*Location, error) {
	region, err := parseSarifRegion(issue)
	if err != nil {
		return nil, err
	}
	artifactLocation := parseSarifArtifactLocation(issue, rootPaths)
	return NewLocation(NewPhysicalLocation(artifactLocation, region)), nil
}

func parseSarifArtifactLocation(issue *gosec.Issue, rootPaths []string) *ArtifactLocation {
	var filePath string
	for _, rootPath := range rootPaths {
		if strings.HasPrefix(issue.File, rootPath) {
			filePath = strings.Replace(issue.File, rootPath+"/", "", 1)
		}
	}
	return NewArtifactLocation(filePath)
}

func parseSarifRegion(issue *gosec.Issue) (*Region, error) {
	lines := strings.Split(issue.Line, "-")
	startLine, err := strconv.Atoi(lines[0])
	if err != nil {
		return nil, err
	}
	endLine := startLine
	if len(lines) > 1 {
		endLine, err = strconv.Atoi(lines[1])
		if err != nil {
			return nil, err
		}
	}
	col, err := strconv.Atoi(issue.Col)
	if err != nil {
		return nil, err
	}
	snippet := NewArtifactContent(issue.Code)
	return NewRegion(startLine, endLine, col, col, "go").WithSnippet(snippet), nil
}

func getSarifLevel(s string) Level {
	switch s {
	case "LOW":
		return Warning
	case "MEDIUM":
		return Error
	case "HIGH":
		return Error
	default:
		return Note
	}
}
