/*
 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
 * openFuyao is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

// Package report provides compliance report generation in HTML and JSON formats.
// It combines scan results with knowledge base rule definitions to produce
// enriched, human-readable reports.
package report

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"strings"
	"sync"
	"time"

	ctrl "sigs.k8s.io/controller-runtime"

	compliancev1alpha1 "gitcode.com/openFuyao/compliance-operator/api/v1alpha1"
	"gitcode.com/openFuyao/compliance-operator/pkg/unified"
)

// generatorLog is the package-level logger for report generation operations.
var generatorLog = ctrl.Log.WithName("report-generator")

// Cached template and knowledge base (initialized once via sync.Once, reused across calls)
var (
	reportTemplate     *template.Template
	reportTemplateOnce sync.Once
	knowledgeBase      *KnowledgeBase
	knowledgeBaseOnce  sync.Once
)

// getReportTemplate returns the cached parsed HTML template.
// The template is compiled once on first access and reused for all subsequent calls.
func getReportTemplate() *template.Template {
	reportTemplateOnce.Do(func() {
		funcMap := template.FuncMap{
			"mulf": func(a, b float64) float64 { return a * b },
			"divf": func(a, b float64) float64 {
				if b == 0 {
					return 0
				}
				return a / b
			},
			"toFloat": func(i int) float64 { return float64(i) },
			"toLower": func(s string) string { return strings.ToLower(s) },
			"add":     func(a, b int) int { return a + b },
			"filterByStatus": func(results []unified.RenderedCheckResult, status string) []unified.RenderedCheckResult {
				var filtered []unified.RenderedCheckResult
				for _, r := range results {
					if r.Status == status {
						filtered = append(filtered, r)
					}
				}
				return filtered
			},
			"filterOtherStatuses": func(results []unified.RenderedCheckResult) []unified.RenderedCheckResult {
				var filtered []unified.RenderedCheckResult
				for _, r := range results {
					if r.Status != unified.StatusFail && r.Status != unified.StatusWarn && r.Status != unified.StatusError {
						filtered = append(filtered, r)
					}
				}
				return filtered
			},
			"nodeAnchor": func(nodeName string) string {
				return "node-" + strings.ToLower(strings.ReplaceAll(nodeName, " ", "-"))
			},
		}
		tmpl, err := template.New("report").Funcs(funcMap).Parse(htmlTemplate)
		if err != nil {
			panic(fmt.Sprintf("failed to parse report template: %v", err))
		}
		reportTemplate = tmpl
	})
	return reportTemplate
}

// getKnowledgeBase returns the cached knowledge base instance.
// The knowledge base is initialized once on first access.
func getKnowledgeBase() *KnowledgeBase {
	knowledgeBaseOnce.Do(func() {
		knowledgeBase = NewKnowledgeBase()
	})
	return knowledgeBase
}

// ReportData contains all data needed to render a compliance report.
// It is constructed by BuildReportData from Kubernetes custom resources.
type ReportData struct {
	ScanName       string
	ScanID         string
	Scanner        string
	Benchmark      string
	Profile        string
	StartTime      time.Time
	EndTime        time.Time
	Summary        compliancev1alpha1.ScanSummary
	ComplianceRate float64
	NodeSummaries  []compliancev1alpha1.NodeSummary
	NodeResults    []compliancev1alpha1.ComplianceScanNodeResult
	// NodeName and NodeType are set for node-specific reports
	NodeName string
	NodeType string
}

// Generator generates compliance reports in various formats,
// enriching raw scan data with knowledge base rule definitions.
type Generator struct {
	knowledgeBase *KnowledgeBase
}

// NewGenerator creates a new report generator using the cached knowledge base.
func NewGenerator() *Generator {
	return &Generator{
		knowledgeBase: getKnowledgeBase(),
	}
}

// GenerateHTML generates an HTML report and writes it to the provided writer.
// The report data is enriched with knowledge base information before rendering.
func (g *Generator) GenerateHTML(w io.Writer, data *ReportData) error {
	if data == nil {
		return fmt.Errorf("report data is nil")
	}
	generatorLog.V(1).Info("generating HTML report", "scanName", data.ScanName)

	tmpl := getReportTemplate()
	enrichedData := g.enrichReportData(data)

	if err := tmpl.Execute(w, enrichedData); err != nil {
		generatorLog.Error(err, "failed to execute HTML template")
		return fmt.Errorf("failed to execute template: %w", err)
	}

	return nil
}

// GenerateJSON generates a JSON report and writes it to the provided writer.
// The report data is enriched with knowledge base information before encoding.
func (g *Generator) GenerateJSON(w io.Writer, data *ReportData) error {
	if data == nil {
		return fmt.Errorf("report data is nil")
	}
	generatorLog.V(1).Info("generating JSON report", "scanName", data.ScanName)

	enrichedData := g.enrichReportData(data)
	encoder := json.NewEncoder(w)
	encoder.SetIndent("", "  ")
	if err := encoder.Encode(enrichedData); err != nil {
		generatorLog.Error(err, "failed to encode JSON report")
		return fmt.Errorf("failed to encode JSON: %w", err)
	}
	return nil
}

// enrichReportData adds knowledge base information to the report data.
// Only NonPassResults are enriched since PASS items are not stored with full details.
// ComplianceRate is pre-calculated by BuildReportData and not recalculated here.
func (g *Generator) enrichReportData(data *ReportData) *EnrichedReportData {
	enriched := &EnrichedReportData{
		ReportData: *data,
	}

	for _, nodeResult := range data.NodeResults {
		enrichedNode := buildEnrichedNode(nodeResult, g.knowledgeBase, data.Scanner, data.Benchmark)
		enriched.EnrichedNodeResults = append(enriched.EnrichedNodeResults, enrichedNode)
	}

	return enriched
}

// buildEnrichedNode creates an EnrichedNodeResult by looking up rule definitions
// for each non-pass check result in the given node result.
func buildEnrichedNode(nodeResult compliancev1alpha1.ComplianceScanNodeResult, kb *KnowledgeBase, scanner, version string) EnrichedNodeResult {
	enrichedNode := EnrichedNodeResult{
		NodeName:  nodeResult.Spec.NodeName,
		NodeType:  nodeResult.Spec.NodeType,
		Summary:   nodeResult.Spec.Summary,
		PassCount: nodeResult.Spec.PassCount,
	}

	for _, check := range nodeResult.Spec.NonPassResults {
		rendered := enrichCheckResult(check, kb, scanner, version)
		enrichedNode.RenderedResults = append(enrichedNode.RenderedResults, rendered)
	}

	return enrichedNode
}

// enrichCheckResult looks up the rule definition for a check and returns a RenderedCheckResult.
// If the rule is not found, a placeholder definition is used.
func enrichCheckResult(check compliancev1alpha1.UnifiedCheckResult, kb *KnowledgeBase, scanner, version string) unified.RenderedCheckResult {
	rule, err := kb.Lookup(scanner, version, check.ID)
	if err != nil {
		generatorLog.V(1).Info("rule definition not available", "scanner", scanner, "ruleID", check.ID)
		return unified.RenderedCheckResult{
			UnifiedCheckResult: check,
			RuleDefinition: unified.RuleDefinition{
				ID:          check.ID,
				Description: "Rule definition not available",
				Severity:    "unknown",
			},
		}
	}
	return unified.RenderedCheckResult{
		UnifiedCheckResult: check,
		RuleDefinition:     *rule,
	}
}

// EnrichedReportData extends ReportData with knowledge base information
// for use in HTML and JSON report rendering.
type EnrichedReportData struct {
	ReportData
	EnrichedNodeResults []EnrichedNodeResult
}

// EnrichedNodeResult contains enriched node result data for report rendering,
// combining node metadata with rendered check results that include rule definitions.
type EnrichedNodeResult struct {
	NodeName        string
	NodeType        string
	Summary         compliancev1alpha1.ScanSummary
	PassCount       int
	RenderedResults []unified.RenderedCheckResult
}

// BuildReportData constructs ReportData from a ComplianceScan, ComplianceScanResult,
// and a list of ComplianceScanNodeResult resources. If nodeName is non-empty,
// the data is filtered to that specific node only.
func BuildReportData(scan *compliancev1alpha1.ComplianceScan, result *compliancev1alpha1.ComplianceScanResult, nodeResults []compliancev1alpha1.ComplianceScanNodeResult, nodeName string) *ReportData {
	if scan == nil || result == nil {
		return &ReportData{}
	}
	generatorLog.V(1).Info("building report data", "scanName", scan.Name, "nodeCount", len(nodeResults))

	data := &ReportData{
		ScanName:      scan.Name,
		ScanID:        result.Spec.ScanID,
		Scanner:       string(result.Spec.Scanner),
		Benchmark:     result.Spec.Benchmark,
		Profile:       result.Spec.Profile,
		StartTime:     result.Spec.ScanStartTime.Time,
		EndTime:       result.Spec.ScanEndTime.Time,
		Summary:       result.Spec.Summary,
		NodeSummaries: result.Spec.NodeSummaries,
		NodeResults:   nodeResults,
	}

	// Clear scanner-irrelevant fields to avoid stale data from CR spec
	switch data.Scanner {
	case "kube-bench":
		data.Profile = "" // kube-bench doesn't use profiles
	case "openscap":
		data.Benchmark = "" // OpenSCAP uses profiles, not CIS benchmarks
	default:
		generatorLog.Info("unknown scanner type, keeping all fields", "scanner", data.Scanner)
	}

	if nodeName != "" {
		filterReportDataByNode(data, nodeName, result, nodeResults)
	}

	scoredTotal := data.Summary.Pass + data.Summary.Fail
	if scoredTotal > 0 {
		data.ComplianceRate = float64(data.Summary.Pass) / float64(scoredTotal) * 100.0
	}

	return data
}

// filterReportDataByNode narrows the report data to a single node by filtering
// node summaries and node results to only include entries matching the given node name.
func filterReportDataByNode(data *ReportData, nodeName string, result *compliancev1alpha1.ComplianceScanResult, nodeResults []compliancev1alpha1.ComplianceScanNodeResult) {
	data.NodeName = nodeName
	data.NodeType, data.Summary = findNodeSummary(result.Spec.NodeSummaries, nodeName)
	data.NodeResults = filterNodeResults(nodeResults, nodeName)

	if len(data.NodeResults) == 0 {
		generatorLog.Info("no matching node result found for node filter", "nodeName", nodeName)
	}
}

// findNodeSummary finds the node summary and type for a given node name.
func findNodeSummary(summaries []compliancev1alpha1.NodeSummary, nodeName string) (string, compliancev1alpha1.ScanSummary) {
	for _, ns := range summaries {
		if ns.NodeName == nodeName {
			return ns.NodeType, ns.Summary
		}
	}
	return "", compliancev1alpha1.ScanSummary{}
}

// filterNodeResults filters node results to only include the entry matching the given node name.
func filterNodeResults(nodeResults []compliancev1alpha1.ComplianceScanNodeResult, nodeName string) []compliancev1alpha1.ComplianceScanNodeResult {
	for _, nr := range nodeResults {
		if nr.Spec.NodeName == nodeName {
			return []compliancev1alpha1.ComplianceScanNodeResult{nr}
		}
	}
	return nil
}