package audit

import (
	"sort"
	"strings"
)

// runHierarchyImpl checks the ISO 12100 / EN 12100 risk-reduction
// hierarchy on the generated mitigation set: every safety-relevant
// hazard should have at least one "inherently safe design" measure
// (design) and additionally either a guarding/protective device
// (protection) or an information-for-use measure (information).
//
// Cyber-, ergonomic-, and software-only hazards have looser
// expectations — design alone or information alone may legitimately
// suffice. The audit reports which level is missing, not whether the
// remaining measures are individually correct. That is a different
// check (E2 — semantic quality), out of scope here.
func init() {
	runHierarchyImpl = runHierarchy
}

// hazardExpectsProtection lists hazard categories where a pure
// design+information combination is usually not enough — the engine
// should produce at least one explicit protective measure (guard,
// interlock, sensor, presence detector, …).
var hazardExpectsProtection = map[string]bool{
	"mechanical_hazard":     true,
	"electrical_hazard":     true,
	"thermal_hazard":        true,
	"pneumatic_hydraulic":   true,
	"radiation_hazard":      true,
	"laser_hazard":          true,
	"fire_explosion_hazard": true,
	"chemical_hazard":       true,
}

func runHierarchy(hazards, mitigations []map[string]any) HierarchyReport {
	report := HierarchyReport{TotalHazards: len(hazards)}

	// Index mitigations by hazard_id
	byHazard := map[string][]map[string]any{}
	for _, m := range mitigations {
		hid, _ := m["hazard_id"].(string)
		if hid == "" {
			continue
		}
		byHazard[hid] = append(byHazard[hid], m)
	}

	for _, h := range hazards {
		hid, _ := h["id"].(string)
		category, _ := h["category"].(string)
		name, _ := h["name"].(string)

		levels := levelsForHazard(byHazard[hid])
		missing := expectedMissing(category, levels)

		if len(missing) == 0 {
			report.Complete++
			continue
		}
		for _, m := range missing {
			switch m {
			case "design":
				report.MissingDesign++
			case "protection":
				report.MissingProtection++
			case "information":
				report.MissingInfo++
			}
		}
		report.IncompleteHazards = append(report.IncompleteHazards, HazardHierarchyResult{
			HazardID:      hid,
			Name:          name,
			Category:      category,
			Levels:        levels,
			MissingLevels: missing,
		})
	}

	// Sort: protection-missing first (most consequential), then by category
	sort.Slice(report.IncompleteHazards, func(i, j int) bool {
		a := report.IncompleteHazards[i]
		b := report.IncompleteHazards[j]
		ap := contains(a.MissingLevels, "protection")
		bp := contains(b.MissingLevels, "protection")
		if ap != bp {
			return ap
		}
		return a.Category < b.Category
	})
	return report
}

// levelsForHazard returns the distinct reduction-type levels present
// for a hazard's mitigation set. Possible values: design, protection,
// information.
func levelsForHazard(mits []map[string]any) []string {
	seen := map[string]bool{}
	for _, m := range mits {
		rt, _ := m["reduction_type"].(string)
		switch strings.ToLower(rt) {
		case "design":
			seen["design"] = true
		case "protection", "protective":
			seen["protection"] = true
		case "information":
			seen["information"] = true
		}
	}
	var out []string
	for k := range seen {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

// expectedMissing returns the levels that the hierarchy demands but
// the mitigation set does not provide.
//
// Rule:
//   - Every hazard with mitigations should have a design measure.
//   - Categories in hazardExpectsProtection additionally need a
//     protection measure.
//   - All hazards should have an information measure unless they
//     already have both design + protection (the information layer
//     can then be considered subsumed for the audit's purpose; the
//     real engine usually still adds it).
func expectedMissing(category string, present []string) []string {
	have := toBoolSet(present)
	var missing []string
	if !have["design"] {
		missing = append(missing, "design")
	}
	if hazardExpectsProtection[category] && !have["protection"] {
		missing = append(missing, "protection")
	}
	// Information is only flagged if both design and protection are
	// also absent — otherwise too noisy. We still surface the case
	// where information is the SOLE present level: that means the
	// hazard is mitigated only by warning labels, which is rarely
	// adequate.
	if !have["information"] && !have["design"] && !have["protection"] {
		missing = append(missing, "information")
	}
	return missing
}

func contains(list []string, target string) bool {
	for _, x := range list {
		if x == target {
			return true
		}
	}
	return false
}
