package main

import (
	"fmt"
	"slices"
	"sort"
	"strconv"
	"strings"
	"time"
)

type GoCVECounting struct {
	Tier1Go, Tier1NonGo, NonTier1Go, NonTier1NonGo int
}
type ImageCVECount struct {
	Release, Type string
}

type TeamImages struct {
	NonMirroredImage, MirroredImage, Opened7Days, Closed7Days int
}

const GO_STDLIB = "stdlib"

// Function that counts all CVEs based on the following structure with struct
// ImageCVECount{} as the map key:
//
// map[ImageCVECount{Release, Type string}]int
//
// This logic makes easier to iterate over the CVE reports and then in one line
// do the couting and assignments based on the scanned `Release` version and the
// `Type` that we want, for example: `tier1-<severity>`, `tier1-total`, mirrored
// vs. non-mirrored CVEs etc.
func countCVEs(reports ReportsPerRelease, imagesVulnCount ReportsImageCVECount) map[ImageCVECount]int {
	count := make(map[ImageCVECount]int)

	// Iterate over the map[releases]
	for rel, rows := range reports {
		imagesTier1 := make(map[string]int)
		imagesMirrored := make(map[string]int)
		imagesNonMirrored := make(map[string]int)

		// Iterate over all CVEs (rows) of the release
		for _, row := range rows {
			if row[COL_STATUS] == CONFIG.CVES.Status.NotAffected {
				continue
			}

			sev := strings.ToLower(row[COL_SEVERITY])
			count[ImageCVECount{rel, sev}]++
			count[ImageCVECount{rel, "total-cves"}]++

			image := strings.Split(row[COL_IMAGE], ":")[0]
			if slices.Contains(CONFIG.Images.Tier1, image) {
				count[ImageCVECount{rel, "tier1-" + sev}]++
				count[ImageCVECount{rel, "tier1-total"}]++
				imagesTier1[row[COL_IMAGE]]++
			}

			if row[COL_MIRRORED] == "true" {
				count[ImageCVECount{rel, "mirrored-cves"}]++
				imagesMirrored[row[COL_IMAGE]]++
			} else {
				count[ImageCVECount{rel, "non-mirrored-cves"}]++
				imagesNonMirrored[row[COL_IMAGE]]++
			}
		}

		count[ImageCVECount{rel, "total-images"}] = len(imagesVulnCount[rel])
		count[ImageCVECount{rel, "total-non-mirrored-images"}] = len(imagesNonMirrored)
		count[ImageCVECount{rel, "total-mirrored-images"}] = len(imagesMirrored)
		count[ImageCVECount{rel, "total-tier1-images"}] = len(imagesTier1)
	}

	return count
}

func dashboardStatusIssue(data string) {
	fmt.Println("Updating dashboard issue")
	fmt.Printf("Outputing dashboard's issue content\n%s\n", data)

	i := Image{}
	i.IssueTitle = CONFIG.Repo.Dashboard.Title
	i.IssueBody = data
	i.IssueLabels = append(i.IssueLabels, CONFIG.Repo.Dashboard.Label)

	dashboardIssue := repoIssuesGet("all", i.IssueLabels, i.IssueTitle)

	if issue, ok := dashboardIssue[i.IssueTitle]; ok {
		i.IssueID = issue.Number
		_, _ = repoIssueCreateUpdateClose("update", i)
	} else {
		_, _ = repoIssueCreateUpdateClose("create", i)
	}
}

func daysDiff(opened int, closed int) string {
	var str string
	if opened > 0 {
		str = "+"
	}
	str = str + strconv.Itoa(opened) + "/"
	if closed > 0 {
		str = str + "-"
	}
	str = str + strconv.Itoa(closed)
	return str
}

// Function that generates the statistics charts for the current latest
// release of Rancher.
//
// The charts follow the Mermaid diagrams support by GH (see
// https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams
// for more information).
//
// The generated charts are:
// - chart1: Total tier 1 images with CVEs per team in <current latest release>
// - chart2: Total CVEs in tier 1 images in <current latest release>
// - chart3: Total CVEs (critical + high + ...) per image type in <current latest release>
// - chart4: Total image types in <current latest release>
// - chart5: Go related CVEs, in its stdlib, in tier 1 image in <current latest release>
func generateRancherLatestCharts(reports ReportsPerRelease, imagesVulnCount ReportsImageCVECount, goCount GoCVECounting, release string) string {
	version := strings.ReplaceAll(release, "rancher/", "")
	title := "### Statistics charts for Rancher " + version

	chartType := "```mermaid\npie showData title "
	chartTypeEnd := "```"

	teamsTier1Images := getTeamsTier1Images()
	vulnCount := countCVEs(reports, imagesVulnCount)

	chart1 := chartType + fmt.Sprintf("Total tier 1 images with CVEs per team in %s\n", version)
	for team, images := range teamsTier1Images {
		count := 0
		for _, image := range images {
			for key, vulns := range imagesVulnCount[release] {
				if strings.HasPrefix(key, image+":") && (vulns["total"] > 0) {
					count++
				}
			}
		}
		chart1 = chart1 + fmt.Sprintf("\"%s\": %d\n", team, count)
	}
	chart1 = chart1 + chartTypeEnd

	chart2 := chartType + fmt.Sprintf("Total CVEs in tier 1 images in %s\n", version)
	for _, sev := range strings.Split(strings.ToLower(CONFIG.Scanner.Severity), ",") {
		chart2 = chart2 + fmt.Sprintf("\"%s\": %d\n", sev, vulnCount[ImageCVECount{release, "tier1-" + sev}])
	}
	chart2 = chart2 + chartTypeEnd

	chart3 := chartType + fmt.Sprintf("Total CVEs per image type in %s\n", version)
	chart3 = chart3 + fmt.Sprintf("\"tier 1\": %d\n", vulnCount[ImageCVECount{release, "tier1-total"}])
	chart3 = chart3 + fmt.Sprintf("\"non-mirrored\": %d\n", vulnCount[ImageCVECount{release, "non-mirrored-cves"}])
	chart3 = chart3 + fmt.Sprintf("\"mirrored\": %d\n", vulnCount[ImageCVECount{release, "mirrored-cves"}])
	chart3 = chart3 + chartTypeEnd

	chart4 := chartType + fmt.Sprintf("Total image types in %s\n", version)
	chart4 = chart4 + fmt.Sprintf("\"tier 1\": %d\n", vulnCount[ImageCVECount{release, "total-tier1-images"}])
	chart4 = chart4 + fmt.Sprintf("\"non-mirrored\": %d\n", vulnCount[ImageCVECount{release, "total-non-mirrored-images"}])
	chart4 = chart4 + fmt.Sprintf("\"mirrored\": %d\n", vulnCount[ImageCVECount{release, "total-mirrored-images"}])
	chart4 = chart4 + chartTypeEnd

	chart5 := chartType + fmt.Sprintf("Go stdlib CVEs in tier 1 in %s\n", version)
	chart5 = chart5 + fmt.Sprintf("\"tier 1 Go related\": %d\n", goCount.Tier1Go)
	chart5 = chart5 + fmt.Sprintf("\"tier 1 non-Go related\": %d\n", goCount.Tier1NonGo)
	chart5 = chart5 + chartTypeEnd

	return fmt.Sprintf("%s\n\n%s\n\n%s\n\n%s\n\n%s\n\n%s\n", title, chart1, chart2, chart3, chart4, chart5)
}

// Function that generates a markdown table, used in the image-scanning GH
// dashboard issue, that has the following contents:
//
// | - | v2.7.12 | v2.7-head | v2.8.3 | v2.8-head | v2.9-head |
// | - | - | - | - | - | - |
// | **Tier 1 images CVEs (26 unique images)** | | | | | |
// | Tier 1 images critical CVEs | 224 | 242 | 70 | 66 | 105 |
// | Tier 1 images high CVEs | 3344 | 3659 | 1250 | 1124 | 1505 |
// | Tier 1 images total CVEs | 3568 | 3901 | 1320 | 1190 | 1610 |
// | Total number of tier 1 images (multiple versions) | 80 | 82 | 75 | 77 | 87 |
// | | | | | | |
// | **Non-mirrored x mirrored CVEs** | | | | | |
// | Non mirrored CVEs | 18611 | 18866 | 6835 | 6708 | 5581 |
// | Mirrored CVEs | 17659 | 17668 | 7200 | 7171 | 5241 |
// | Total CVEs | 36270 | 36534 | 14035 | 13879 | 10822 |
// | | | | | | |
// | **Non-mirrored x mirrored images** | | | | | |
// | Non mirrored images | 243 | 254 | 177 | 188 | 184 |
// | Mirrored images | 388 | 410 | 255 | 283 | 241 |
// | Total images | 631 | 664 | 432 | 471 | 425 |
func generateRancherReleasesPerCVEsTable(reports ReportsPerRelease, imagesVulnCount ReportsImageCVECount, sk []string) string {
	title := "### Rancher releases"
	header := "| |"
	subheader := "| - |"
	emptySubheader := "| |"

	tier1Header := fmt.Sprintf("| **Tier 1 images CVEs (%d unique images)** |", len(CONFIG.Images.Tier1))
	tier1Critical := "| Tier 1 images critical CVEs |"
	tier1High := "| Tier 1 images high CVEs |"
	tier1Total := "| Tier 1 images total CVEs |"
	tier1TotalImages := "| Total number of tier 1 images (multiple versions) |"

	nonMirroredHeader := "| **Non-mirrored x mirrored CVEs** |"
	nonMirroredCVEs := "| Non-mirrored CVEs |"
	mirroredCVEs := "| Mirrored CVEs |"
	totalCVEs := "| Total CVEs |"

	nonMirroredImagesHeader := "| **Non-mirrored x mirrored images** |"
	nonMirroredImages := "| Non-mirrored images |"
	mirroredImages := "| Mirrored images |"
	totalImages := "| Total images |"

	vulnCount := countCVEs(reports, imagesVulnCount)

	for _, v := range sk {
		header = header + fmt.Sprintf(" %s |", strings.ReplaceAll(v, "rancher/", ""))
		subheader = subheader + " - |"
		emptySubheader = emptySubheader + " |"

		tier1Header = tier1Header + " |"
		tier1Critical = tier1Critical + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "tier1-critical"}])
		tier1High = tier1High + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "tier1-high"}])
		tier1Total = tier1Total + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "tier1-total"}])
		tier1TotalImages = tier1TotalImages + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-tier1-images"}])

		nonMirroredHeader = nonMirroredHeader + " |"
		nonMirroredCVEs = nonMirroredCVEs + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "non-mirrored-cves"}])
		mirroredCVEs = mirroredCVEs + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "mirrored-cves"}])
		totalCVEs = totalCVEs + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-cves"}])

		nonMirroredImagesHeader = nonMirroredImagesHeader + " |"
		nonMirroredImages = nonMirroredImages + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-non-mirrored-images"}])
		mirroredImages = mirroredImages + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-mirrored-images"}])
		totalImages = totalImages + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-images"}])

	}

	return fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
		title, header, subheader, tier1Header, tier1Critical, tier1High,
		tier1Total, tier1TotalImages, emptySubheader, nonMirroredHeader,
		nonMirroredCVEs, mirroredCVEs, totalCVEs, emptySubheader,
		nonMirroredImagesHeader, nonMirroredImages, mirroredImages, totalImages)
}

// generateOtherReleasesPerCVEsTable() is a function similar to
// generateRancherReleasesPerCVEsTable() that generates a simplified table for
// Harvester, Longhorn and other products.
func generateOtherReleasesPerCVEsTable(reports ReportsPerRelease,
	imagesVulnCount ReportsImageCVECount, product string, sk []string) string {

	productTitle := strings.ToUpper(string(product[0])) + product[1:]
	title := fmt.Sprintf("### %s releases", productTitle)
	header := fmt.Sprintf("| %s CVEs |", productTitle)
	subheader := "| - |"

	critical := "| Critical CVEs |"
	high := "| High CVEs |"
	total := "| Total CVEs |"
	totalImages := "| Total images |"

	vulnCount := countCVEs(reports, imagesVulnCount)

	for _, v := range sk {
		header = header + fmt.Sprintf(" %s |", strings.ReplaceAll(v, product+"/", ""))
		subheader = subheader + " - |"

		critical = critical + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "critical"}])
		high = high + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "high"}])
		total = total + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-cves"}])
		totalImages = totalImages + fmt.Sprintf(" %d |", vulnCount[ImageCVECount{v, "total-images"}])
	}

	return fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s\n%s\n%s\n\n",
		title, header, subheader, critical, high, total, totalImages)
}

func generateStatisticsPerTeam(release string) string {
	title := `### Statistics per team for Rancher v2.9-head

- Legend:
  - An image with CVE means that at least one vulnerability was identified in the image.
  - New images: new images with CVEs detected in the last 7 days.
  - Fixed images: an image was removed or all identified vulnerabilities were fixed in the last 7 days.

| team | non-mirrored images | mirrored images | total | new images/fixed images<br>(last 7 days)|
| - | - | - | - | - |
`
	teams := getTeamsIssues(release)

	sk := make([]string, 0, len(teams))
	for k := range teams {
		sk = append(sk, k)
	}
	sort.Strings(sk)

	data := title
	for _, k := range sk {
		data = data + fmt.Sprintf("| %s | %d | %d | %d | %s |\n", k, teams[k].NonMirroredImage, teams[k].MirroredImage, teams[k].NonMirroredImage+teams[k].MirroredImage, daysDiff(teams[k].Opened7Days, teams[k].Closed7Days))
	}

	return data
}

func generateStatisticsPerTear1Images(reportsImageCVECount ReportsImageCVECount, release string) string {
	title := `### Tier 1 images CVEs for Rancher v2.9-head

- Legend:
  - An image with CVE means that at least one vulnerability was identified in the image.
`
	imagesTeams := readTeamsFile(CONFIG.ImagesTeamsFile)

	data := title + "\n" + fmt.Sprintf("| image | team | %s | total |\n| - | - | - | - | - |\n", strings.ReplaceAll(strings.ToLower(CONFIG.Scanner.Severity), ",", " | "))

	for _, tier1 := range CONFIG.Images.Tier1 {
		teams, ok := imagesTeams[tier1]
		if !ok {
			continue
		}
		count, ok := reportsImageCVECount[release][tier1]
		if !ok {
			continue
		}

		data = data + fmt.Sprintf("| %s | %s |", tier1, strings.Join(teams, "<br>"))
		for _, sev := range strings.Split(CONFIG.Scanner.Severity, ",") {
			data = data + fmt.Sprintf(" %d |", count[sev])
		}
		data = data + fmt.Sprintf(" %d |\n", count["total"])
	}

	return data
}

// Function that returns a summary of GH issues' states and images types per
// team.
//
// Example:
// map["rke2-k3s"]TeamImages{NonMirroredImage:1, MirroredImage:0, Opened7Days:1, Closed7Days:3}
//
// The struct TeamImages{} also contains the number of issues assigned to the
// team that were created and closed in the current/previous week.
func getTeamsIssues(release string) map[string]*TeamImages {
	teams := make(map[string]*TeamImages)
	timeNow := time.Now()
	timePreviousWeek := timeNow.AddDate(0, 0, -7)

	repoAuth()
	issues := repoIssuesGet("all", []string{"cve/release/" + release}, CONFIG.Repo.Issue.Title)

	for _, issue := range issues {
		for _, label := range issue.Labels {
			if !strings.HasPrefix(label, CONFIG.Repo.Issue.TeamPrefix) {
				continue
			}
			label = strings.TrimPrefix(label, "team/")
			if _, ok := teams[label]; !ok {
				teams[label] = &TeamImages{}
			}
			if strings.EqualFold(issue.State, "open") {
				if slices.Contains(issue.Labels, CONFIG.Repo.Issue.LabelPrefix+CONFIG.Repo.Issue.MirroredImageLabel) {
					teams[label].MirroredImage++
				} else {
					teams[label].NonMirroredImage++
				}
				if issue.CreatedAt.After(timePreviousWeek) {
					teams[label].Opened7Days++
				}

			} else if strings.EqualFold(issue.State, "closed") && (issue.ClosedAt.After(timePreviousWeek)) {
				teams[label].Closed7Days++
			}
		}
	}

	return teams
}

// Function that returns the list of tier 1 images owned by each team.
//
// Example:
// map["rke2-k3s"]["rancher/rke2-runtime", "rancher/rke2-upgrade", ...]
func getTeamsTier1Images() map[string][]string {
	imagesTeams := readTeamsFile(CONFIG.ImagesTeamsFile)
	teamsTier1Images := make(map[string][]string)
	teamsTier1Images["unassigned"] = []string{}

	for _, tier1 := range CONFIG.Images.Tier1 {
		teams, ok := imagesTeams[tier1]
		if !ok {
			continue
		}
		for _, team := range teams {
			teamsTier1Images[team] = append(teamsTier1Images[team], tier1)
		}
	}

	return teamsTier1Images
}

// sortOtherReleaseHeaders() sorts in alphabetical order the Harvester, Longhorn
// and other products release versions. The second return value is the current
// latest -head version of the sorted product or master if available.
//
// It temporarily replaces `-head` per `100-head` in order to make it the
// biggest value in the releases, so `-head` appears as the last release version
// per release line.
// Example: harvester/v1.3.0, harvester/v1.3-head
func sortOtherReleaseHeaders(reports ReportsPerRelease, product string,
	productMaster string) ([]string, string) {

	master := false
	masterRelease := productMaster
	sk := make([]string, 0, len(reports))

	for key := range reports {
		if strings.HasPrefix(key, product+"/") {
			if key == masterRelease {
				master = true
				continue
			}
			sk = append(sk, strings.ReplaceAll(key, "-head", "100-head"))
		}
	}

	sort.Strings(sk)

	for k, v := range sk {
		sk[k] = strings.ReplaceAll(v, "100-head", "-head")
	}

	if master {
		sk = append(sk, masterRelease)
		return sk, masterRelease
	}

	return sk, sk[len(sk)-1]
}

// Function that sorts in alphabetical order the Rancher release versions.
// The second return value is the current latest -head version of Rancher.
//
// Temporarily replace `-head` per `100-head` in order to make it the biggest
// value in the releases, so `-head` appears as the last release version per
// release line.
// Example: v2.7.12, v2.7-head, v2.8.3, v2.8-head, v2.9-head
func sortRancherReleaseHeaders(reports ReportsPerRelease) ([]string, string) {
	sk := make([]string, 0, len(reports))

	for key := range reports {
		if strings.HasPrefix(key, "rancher/") {
			sk = append(sk, strings.ReplaceAll(key, "-head", "100-head"))
		}
	}

	sort.Strings(sk)

	for k, v := range sk {
		sk[k] = strings.ReplaceAll(v, "100-head", "-head")
	}

	return sk, sk[len(sk)-1]
}

func sumGoRelateCVEs(reports ReportsPerRelease, release string) GoCVECounting {
	var count GoCVECounting
	for _, row := range reports[release] {
		if isTier1Image(row[COL_IMAGE]) {
			if row[COL_PACKAGE_NAME] == GO_STDLIB {
				count.Tier1Go++
			} else {
				count.Tier1NonGo++
			}
		} else if row[COL_MIRRORED] == "false" {
			if row[COL_PACKAGE_NAME] == GO_STDLIB {
				count.NonTier1Go++
			} else {
				count.NonTier1NonGo++
			}
		}
	}
	return count
}

func mainGenerateDashboard(cveRecords CvesRecords, images Images) {
	data := `# Image-Scanning Dashboard

## Image-Scanning Statistics

- Legend:
  - An image with CVE means that at least one vulnerability was identified in the image.
`
	data = data + fmt.Sprintf("  - Currently we scan only **%s** vulnerabilities.\n\n",
		strings.ReplaceAll(strings.ToLower(CONFIG.Scanner.Severity), ",", ", "))

	fmt.Println("Generating dashboard")

	reports := reportExtractData(cveRecords)
	rancherSortedReleases, rancherCurrentRelease := sortRancherReleaseHeaders(reports.ReportsPerRelease)
	harvesterSortedReleases, _ := sortOtherReleaseHeaders(reports.ReportsPerRelease, "harvester", "harvester/master")
	longhornSortedReleases, _ := sortOtherReleaseHeaders(reports.ReportsPerRelease, "longhorn", "longhorn/master")
	goCount := sumGoRelateCVEs(reports.ReportsPerRelease, rancherCurrentRelease)

	data = data + generateRancherReleasesPerCVEsTable(reports.ReportsPerRelease, reports.ReportsImageCVECount, rancherSortedReleases) +
		"\n" + generateRancherLatestCharts(reports.ReportsPerRelease, reports.ReportsImageCVECount, goCount, rancherCurrentRelease) +
		"\n" + generateStatisticsPerTeam(rancherCurrentRelease) +
		"\n" + generateStatisticsPerTear1Images(reports.ReportsImageAggregatedCVECount, rancherCurrentRelease) +
		"\n" + generateOtherReleasesPerCVEsTable(reports.ReportsPerRelease, reports.ReportsImageCVECount, "harvester", harvesterSortedReleases) +
		"\n" + generateOtherReleasesPerCVEsTable(reports.ReportsPerRelease, reports.ReportsImageCVECount, "longhorn", longhornSortedReleases)

	dashboardStatusIssue(data)
}
