/*
 * 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

import (
	"embed"
	"fmt"
	"io/fs"
	"path"
	"strings"

	"gopkg.in/yaml.v3"
	ctrl "sigs.k8s.io/controller-runtime"

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

// cisLoaderLog is the logger for CIS loader operations.
var cisLoaderLog = ctrl.Log.WithName("cis-loader")

//go:embed embed/cis/*/*.yaml
var cisFS embed.FS

// yamlID is a custom type that handles YAML unmarshaling of IDs that may be
// integers, floats, or strings. It always stores the value as a string.
type yamlID string

// UnmarshalYAML implements yaml.Unmarshaler for yamlID.
// It reads the raw scalar value from the YAML node regardless of its
// inferred type (int, float, string) and stores it as a string.
func (id *yamlID) UnmarshalYAML(value *yaml.Node) error {
	*id = yamlID(value.Value)
	return nil
}

// cisControls represents the top-level structure of a kube-bench CIS YAML file.
// The YAML files have top-level keys: controls (null), version, id, text, type, groups.
type cisControls struct {
	Version string     `yaml:"version"`
	ID      yamlID     `yaml:"id"`
	Text    string     `yaml:"text"`
	Type    string     `yaml:"type"`
	Groups  []cisGroup `yaml:"groups"`
}

// cisGroup represents a group of checks within a CIS control section.
type cisGroup struct {
	ID     yamlID     `yaml:"id"`
	Text   string     `yaml:"text"`
	Checks []cisCheck `yaml:"checks"`
}

// cisCheck represents a single CIS Benchmark check rule.
type cisCheck struct {
	ID          yamlID `yaml:"id"`
	Text        string `yaml:"text"`
	Type        string `yaml:"type"`
	Remediation string `yaml:"remediation"`
	Scored      bool   `yaml:"scored"`
}

// cisCategoryMap maps CIS control type strings to human-readable category names.
var cisCategoryMap = map[string]string{
	"master":       "Control Plane",
	"node":         "Worker Node",
	"etcd":         "etcd",
	"controlplane": "Control Plane",
	"policies":     "Policies",
}

// loadCISRules loads all CIS Benchmark rules from embedded YAML files.
// It returns a map of version → ruleID → RuleDefinition.
// Versions are discovered from subdirectories under embed/cis/.
func loadCISRules() map[string]map[string]unified.RuleDefinition {
	result := make(map[string]map[string]unified.RuleDefinition)

	versions, err := fs.ReadDir(cisFS, "embed/cis")
	if err != nil {
		cisLoaderLog.Error(err, "failed to read CIS embed directory")
		return result
	}

	totalRules := 0
	for _, versionEntry := range versions {
		if !versionEntry.IsDir() {
			continue
		}
		version := versionEntry.Name()
		rules := loadCISVersionRules(version)
		if len(rules) > 0 {
			result[version] = rules
			totalRules += len(rules)
		}
	}

	cisLoaderLog.Info("CIS knowledge base loaded", "versions", len(result), "totalRules", totalRules)
	return result
}

// loadCISVersionRules loads all CIS rules for a specific version directory.
func loadCISVersionRules(version string) map[string]unified.RuleDefinition {
	rules := make(map[string]unified.RuleDefinition)

	versionDir := path.Join("embed/cis", version)
	files, err := fs.ReadDir(cisFS, versionDir)
	if err != nil {
		cisLoaderLog.Error(err, "failed to read CIS version directory", "version", version)
		return rules
	}

	for _, fileEntry := range files {
		if !isCISYAMLFile(fileEntry) {
			continue
		}
		fileRules := loadCISFile(version, fileEntry.Name())
		for id, rule := range fileRules {
			rules[id] = rule
		}
	}

	return rules
}

// isCISYAMLFile checks if a directory entry is a non-directory YAML file.
func isCISYAMLFile(entry fs.DirEntry) bool {
	return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml")
}

// loadCISFile reads and parses a single CIS YAML file, returning its rule definitions.
func loadCISFile(version, filename string) map[string]unified.RuleDefinition {
	filePath := path.Join("embed/cis", version, filename)
	data, err := fs.ReadFile(cisFS, filePath)
	if err != nil {
		cisLoaderLog.Error(err, "failed to read CIS file", "file", filePath)
		return nil
	}

	var controls cisControls
	if err := yaml.Unmarshal(data, &controls); err != nil {
		cisLoaderLog.Error(err, "failed to parse CIS YAML", "file", filePath)
		return nil
	}

	benchmark := controls.Version
	if benchmark == "" {
		benchmark = fmt.Sprintf("cis-%s", version)
	}

	category := mapCISCategory(controls.Type)
	return extractCISChecks(controls, benchmark, category)
}

// extractCISChecks extracts rule definitions from CIS controls into a flat map.
func extractCISChecks(controls cisControls, benchmark, category string) map[string]unified.RuleDefinition {
	rules := make(map[string]unified.RuleDefinition)
	for _, group := range controls.Groups {
		for _, check := range group.Checks {
			ruleID := string(check.ID)
			if ruleID == "" {
				continue
			}
			severity := "info"
			if check.Scored {
				// Map severity based on the section number
				switch {
				case strings.HasPrefix(ruleID, "1.") || strings.HasPrefix(ruleID, "2."):
					severity = "high"
				case strings.HasPrefix(ruleID, "3.") || strings.HasPrefix(ruleID, "4."):
					severity = "medium"
				default:
					severity = "low"
				}
			}
			rules[ruleID] = unified.RuleDefinition{
				ID:          ruleID,
				Description: check.Text,
				Remediation: strings.TrimSpace(check.Remediation),
				Severity:    severity,
				Category:    category,
				Scored:      check.Scored,
				Benchmark:   benchmark,
			}
		}
	}
	return rules
}

// mapCISCategory maps a CIS controls type field to a human-readable category name.
func mapCISCategory(controlType string) string {
	if mapped, ok := cisCategoryMap[controlType]; ok {
		return mapped
	}
	return "General"
}