package fortify

import (
	"bytes"
	"crypto/sha1"
	"encoding/json"
	"fmt"
	"math"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/SAP/jenkins-library/pkg/format"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/piperutils"
	"github.com/SAP/jenkins-library/pkg/reporting"
	"github.com/piper-validation/fortify-client-go/models"

	"github.com/pkg/errors"
)

type FortifyReportData struct {
	ToolName                            string                  `json:"toolName"`
	ToolInstance                        string                  `json:"toolInstance"`
	ProjectID                           int64                   `json:"projectID"`
	ProjectName                         string                  `json:"projectName"`
	ProjectVersion                      string                  `json:"projectVersion"`
	ProjectVersionID                    int64                   `json:"projectVersionID"`
	Violations                          int                     `json:"violations"`
	CorporateTotal                      int                     `json:"corporateTotal"`
	CorporateAudited                    int                     `json:"corporateAudited"`
	AuditAllTotal                       int                     `json:"auditAllTotal"`
	AuditAllAudited                     int                     `json:"auditAllAudited"`
	SpotChecksTotal                     int                     `json:"spotChecksTotal"`
	SpotChecksAudited                   int                     `json:"spotChecksAudited"`
	SpotChecksGap                       int                     `json:"spotChecksGap"`
	Suspicious                          int                     `json:"suspicious"`
	Exploitable                         int                     `json:"exploitable"`
	Suppressed                          int                     `json:"suppressed"`
	AtleastOneSpotChecksCategoryAudited bool                    `json:"atleastOneSpotChecksCategoryAudited"`
	IsSpotChecksPerCategoryAudited      bool                    `json:"isSpotChecksPerCategoryAudited"`
	URL                                 string                  `json:"url"`
	SpotChecksCategories                *[]SpotChecksAuditCount `json:"spotChecksCategories"`
}

type SpotChecksAuditCount struct {
	Audited int    `json:"audited"`
	Total   int    `json:"total"`
	Type    string `json:"type"`
}

func CreateCustomReport(data FortifyReportData, issueGroups []*models.ProjectVersionIssueGroup) reporting.ScanReport {

	scanReport := reporting.ScanReport{
		ReportTitle: "Fortify SAST Report",
		Subheaders: []reporting.Subheader{
			{Description: "Fortify project name", Details: data.ProjectName},
			{Description: "Fortify project version", Details: data.ProjectVersion},
			{Description: "Fortify URL", Details: data.URL},
		},
		Overview: []reporting.OverviewRow{
			{Description: "Number of compliance violations", Details: fmt.Sprint(data.Violations)},
			{Description: "Number of issues suppressed", Details: fmt.Sprint(data.Suppressed)},
			{Description: "Unaudited corporate issues", Details: fmt.Sprint(data.CorporateTotal - data.CorporateAudited)},
			{Description: "Unaudited audit all issues", Details: fmt.Sprint(data.AuditAllTotal - data.AuditAllAudited)},
			{Description: "Unaudited spot check issues", Details: fmt.Sprint(data.SpotChecksTotal - data.SpotChecksAudited)},
			{Description: "Number of suspicious issues", Details: fmt.Sprint(data.Suspicious)},
			{Description: "Number of exploitable issues", Details: fmt.Sprint(data.Exploitable)},
		},
		ReportTime: time.Now(),
	}

	detailTable := reporting.ScanDetailTable{
		NoRowsMessage: "No findings detected",
		Headers: []string{
			"Issue group",
			"Total count",
			"Audited count",
		},
		WithCounter:   true,
		CounterHeader: "Entry #",
	}

	for _, group := range issueGroups {
		row := reporting.ScanRow{}
		row.AddColumn(fmt.Sprint(*group.CleanName), 0)
		row.AddColumn(fmt.Sprint(*group.TotalCount), 0)
		row.AddColumn(fmt.Sprint(*group.AuditedCount), 0)

		detailTable.Rows = append(detailTable.Rows, row)
	}

	scanReport.DetailTable = detailTable
	scanReport.SuccessfulScan = data.Violations == 0

	return scanReport
}

func CreateJSONReport(reportData FortifyReportData, spotChecksCountByCategory []SpotChecksAuditCount, serverURL string) FortifyReportData {
	reportData.AtleastOneSpotChecksCategoryAudited = true
	reportData.IsSpotChecksPerCategoryAudited = true
	for _, spotChecksElement := range spotChecksCountByCategory {
		if spotChecksElement.Total > 0 && spotChecksElement.Audited == 0 {
			reportData.AtleastOneSpotChecksCategoryAudited = false
		}

		spotCheckMinimumPercentageValue := int(math.Ceil(float64(0.10 * float64(spotChecksElement.Total))))
		if spotChecksElement.Audited < spotCheckMinimumPercentageValue && spotChecksElement.Audited < 10 {
			reportData.IsSpotChecksPerCategoryAudited = false
		}

		if !reportData.IsSpotChecksPerCategoryAudited && !reportData.AtleastOneSpotChecksCategoryAudited {
			break
		}
	}

	reportData.SpotChecksCategories = &spotChecksCountByCategory
	reportData.URL = serverURL + "/html/ssc/version/" + strconv.FormatInt(reportData.ProjectVersionID, 10)
	reportData.ToolInstance = serverURL
	reportData.ToolName = "fortify"

	return reportData
}

func WriteJSONReport(jsonReport FortifyReportData) ([]piperutils.Path, error) {
	utils := piperutils.Files{}
	reportPaths := []piperutils.Path{}

	// Standard JSON Report
	jsonComplianceReportPath := filepath.Join(ReportsDirectory, "piper_fortify_report.json")
	// Ensure reporting directory exists
	if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
		return reportPaths, errors.Wrapf(err, "failed to create report directory")
	}

	file, _ := json.Marshal(jsonReport)
	if err := utils.FileWrite(jsonComplianceReportPath, file, 0666); err != nil {
		log.SetErrorCategory(log.ErrorConfiguration)
		return reportPaths, errors.Wrapf(err, "failed to write fortify json compliance report")
	}
	reportPaths = append(reportPaths, piperutils.Path{Name: "Fortify JSON Compliance Report", Target: jsonComplianceReportPath})

	return reportPaths, nil
}

func WriteSarif(sarif format.SARIF) ([]piperutils.Path, error) {
	utils := piperutils.Files{}
	reportPaths := []piperutils.Path{}

	sarifReportPath := filepath.Join(ReportsDirectory, "result.sarif")
	// Ensure reporting directory exists
	if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
		return reportPaths, errors.Wrapf(err, "failed to create report directory")
	}

	// This solution did not allow for special HTML characters. If this causes any issue, revert l148-l157 with these two
	/*file, _ := json.MarshalIndent(sarif, "", "  ")
	if err := utils.FileWrite(sarifReportPath, file, 0666); err != nil {*/

	// HTML characters will most likely be present: we need to use encode: create a buffer to hold JSON data
	buffer := new(bytes.Buffer)
	// create JSON encoder for buffer
	bufEncoder := json.NewEncoder(buffer)
	// set options
	bufEncoder.SetEscapeHTML(false)
	bufEncoder.SetIndent("", "  ")
	//encode to buffer
	bufEncoder.Encode(sarif)
	log.Entry().Info("Writing file to disk: ", sarifReportPath)
	if err := utils.FileWrite(sarifReportPath, buffer.Bytes(), 0666); err != nil {
		log.SetErrorCategory(log.ErrorConfiguration)
		return reportPaths, errors.Wrapf(err, "failed to write fortify SARIF report")
	}
	reportPaths = append(reportPaths, piperutils.Path{Name: "Fortify SARIF Report", Target: sarifReportPath})

	return reportPaths, nil
}

func WriteCustomReports(scanReport reporting.ScanReport) ([]piperutils.Path, error) {
	utils := piperutils.Files{}
	reportPaths := []piperutils.Path{}

	// ignore templating errors since template is in our hands and issues will be detected with the automated tests
	htmlReport, _ := scanReport.ToHTML()
	htmlReportPath := filepath.Join(ReportsDirectory, "piper_fortify_report.html")
	// Ensure reporting directory exists
	if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
		return reportPaths, errors.Wrapf(err, "failed to create report directory")
	}
	if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil {
		log.SetErrorCategory(log.ErrorConfiguration)
		return reportPaths, errors.Wrapf(err, "failed to write html report")
	}
	reportPaths = append(reportPaths, piperutils.Path{Name: "Fortify Report", Target: htmlReportPath})

	// we do not add the json report to the overall list of reports for now,
	// since it is just an intermediary report used as input for later
	// and there does not seem to be real benefit in archiving it.
	return reportPaths, nil
}

func reportShaFortify(parts []string) string {
	reportShaData := []byte(strings.Join(parts, ","))
	return fmt.Sprintf("%x", sha1.Sum(reportShaData))
}
