/*
 * 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 openscap implements the scanner.Parser interface for OpenSCAP XCCDF output.
// It parses XCCDF XML results and converts them into unified compliance check results,
// extracting Vuln IDs from DISA STIG rule identifiers.
package openscap

import (
	"encoding/xml"
	"fmt"
	"regexp"
	"strings"

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

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

// parserLog is the package-level logger for OpenSCAP parsing operations.
var parserLog = ctrl.Log.WithName("openscap-parser")

// Parser implements the scanner.Parser interface for OpenSCAP XCCDF output.
type Parser struct{}

// vulnIDRegex extracts Vuln IDs from XCCDF rule idref attributes.
// Example: "xccdf_mil.disa.stig_rule_SV-242376r960759_rule" captures "242376".
var vulnIDRegex = regexp.MustCompile(`SV-(\d+)`)

// NewParser creates a new OpenSCAP parser instance.
func NewParser() *Parser {
	return &Parser{}
}

// xccdfBenchmark represents the XCCDF Benchmark XML root element.
// OpenSCAP outputs the full Benchmark document with TestResult nested inside.
type xccdfBenchmark struct {
	XMLName    xml.Name        `xml:"Benchmark"`
	TestResult xccdfTestResult `xml:"TestResult"`
}

// xccdfTestResult represents the XCCDF TestResult XML element.
type xccdfTestResult struct {
	RuleResults []xccdfRuleResult `xml:"rule-result"`
}

// xccdfRuleResult represents a single rule-result element within XCCDF output.
type xccdfRuleResult struct {
	IDRef    string `xml:"idref,attr"`
	Time     string `xml:"time,attr"`
	Severity string `xml:"severity,attr"`
	Version  string `xml:"version,attr"`
	Result   string `xml:"result"`
}

// Parse parses OpenSCAP XCCDF XML output and converts it to unified check results.
// It first attempts to extract content between result markers, falling back to
// direct XML extraction if markers are not present. Results with "notselected"
// status are excluded from the output.
// Supports both <Benchmark> root (OpenSCAP full output) and <TestResult> root formats.
func (p *Parser) Parse(rawOutput string) ([]compliancev1alpha1.UnifiedCheckResult, error) {
	parserLog.V(1).Info("parsing OpenSCAP output", "inputSize", len(rawOutput))

	extracted, err := extractOpenSCAPContent(rawOutput)
	if err != nil {
		return nil, err
	}

	var testResult xccdfTestResult

	// Try parsing as <Benchmark> root first (OpenSCAP full output format)
	var benchmark xccdfBenchmark
	if err := xml.Unmarshal([]byte(extracted), &benchmark); err == nil && len(benchmark.TestResult.RuleResults) > 0 {
		testResult = benchmark.TestResult
	} else {
		// Fallback: try parsing as <TestResult> root directly
		if err := xml.Unmarshal([]byte(extracted), &testResult); err != nil {
			parserLog.Error(err, "failed to parse XCCDF XML")
			return nil, fmt.Errorf("failed to parse XCCDF XML: %w", err)
		}
	}

	results := make([]compliancev1alpha1.UnifiedCheckResult, 0, len(testResult.RuleResults))
	for _, ruleResult := range testResult.RuleResults {
		unifiedResult := convertOpenSCAPResult(ruleResult)
		// Skip notselected results (empty status indicates notselected)
		if unifiedResult.Status != "" {
			results = append(results, unifiedResult)
		}
	}

	parserLog.V(1).Info("OpenSCAP parse complete", "resultCount", len(results))
	return results, nil
}

// extractOpenSCAPContent extracts OpenSCAP XCCDF content from raw output.
// It first attempts marker-based extraction, falling back to direct XML extraction.
func extractOpenSCAPContent(rawOutput string) (string, error) {
	extracted, found := scanner.ExtractBetweenMarkers(rawOutput, scanner.ResultStartMarker, scanner.ResultEndMarker)
	if !found {
		parserLog.V(1).Info("markers not found, attempting direct XML extraction")
		extracted = extractXML(rawOutput)
	}
	if extracted == "" {
		return "", fmt.Errorf("no valid OpenSCAP output found between markers")
	}
	return extracted, nil
}

// convertOpenSCAPResult converts a single OpenSCAP rule result to the unified format.
// The Vuln ID is extracted from the XCCDF idref attribute using a regex pattern.
func convertOpenSCAPResult(rule xccdfRuleResult) compliancev1alpha1.UnifiedCheckResult {
	vulnID := extractVulnID(rule.IDRef)

	// Note: rule.Severity (high/medium/low) from XCCDF is intentionally not propagated.
	// The report generator uses severity from the knowledge base (STIG/CIS definitions)
	// which provides more consistent and detailed severity levels.
	return compliancev1alpha1.UnifiedCheckResult{
		ID:            vulnID,
		Status:        mapOpenSCAPStatus(rule.Result),
		ActualValue:   rule.Result,
		ExpectedValue: "pass", // STIG rules always expect pass
	}
}

// extractVulnID extracts the Vuln ID from an XCCDF rule idref attribute.
// Example: "xccdf_mil.disa.stig_rule_SV-242376r960759_rule" returns "V-242376".
// Falls back to returning the full idref if no pattern match is found.
func extractVulnID(idref string) string {
	matches := vulnIDRegex.FindStringSubmatch(idref)
	if len(matches) < 2 {
		parserLog.V(1).Info("unable to extract Vuln ID from idref", "idref", idref)
		return ""
	}
	return "V-" + matches[1]
}

// openscapStatusMap maps lowercase OpenSCAP XCCDF result values to unified status constants.
// An empty string value indicates the result should be skipped (e.g., "notselected").
var openscapStatusMap = map[string]string{
	"pass":          unified.StatusPass,
	"fail":          unified.StatusFail,
	"error":         unified.StatusError,
	"unknown":       unified.StatusWarn,
	"notapplicable": unified.StatusNotApplicable,
	"notchecked":    unified.StatusNotChecked,
	"informational": unified.StatusInfo,
	"notselected":   "",
}

// mapOpenSCAPStatus maps OpenSCAP XCCDF result values to unified status constants.
// Returns empty string for "notselected" to signal the caller to skip the result.
func mapOpenSCAPStatus(result string) string {
	if s, ok := openscapStatusMap[strings.ToLower(result)]; ok {
		return s
	}
	return unified.StatusWarn
}

// extractXML attempts to extract XCCDF XML from content by locating the XML
// declaration or root element and the appropriate closing tag.
// Supports both <Benchmark> and <TestResult> root elements.
// Returns empty string if the XML boundaries cannot be determined.
func extractXML(content string) string {
	startIdx := strings.Index(content, "<?xml")
	if startIdx == -1 {
		startIdx = strings.Index(content, "<Benchmark")
	}
	if startIdx == -1 {
		startIdx = strings.Index(content, "<TestResult")
	}

	if startIdx == -1 {
		parserLog.V(1).Info("XML start marker not found in output")
		return ""
	}

	// Try </Benchmark> first, then </TestResult>
	endIdx := strings.LastIndex(content, "</Benchmark>")
	if endIdx == -1 || endIdx <= startIdx {
		endIdx = strings.LastIndex(content, "</TestResult>")
		if endIdx == -1 || endIdx <= startIdx {
			parserLog.V(1).Info("XML end marker not found in output")
			return ""
		}
		return content[startIdx : endIdx+len("</TestResult>")]
	}

	return content[startIdx : endIdx+len("</Benchmark>")]
}