// Package audit provides static and runtime audits of the IACE pattern
// engine — finding pattern reachability, library consistency, and
// limits-form coverage gaps without a ground-truth reference.
package audit

import (
	"sort"

	"github.com/breakpilot/ai-compliance-sdk/internal/iace"
)

// ReachabilityResult is the verdict for a single pattern in Method A.
type ReachabilityResult struct {
	PatternID        string   `json:"pattern_id"`
	Name             string   `json:"name_de"`
	Priority         int      `json:"priority"`
	RequiredAllTags  []string `json:"required_tags"`
	UnreachableTags  []string `json:"unreachable_tags,omitempty"`
	Status           string   `json:"status"` // "reachable" | "weakly_reachable" | "unreachable"
	ReachableSources []string `json:"reachable_sources,omitempty"`
	FixSuggestions   []string `json:"fix_suggestions,omitempty"`
}

// ReachabilityReport is the full Method A output.
type ReachabilityReport struct {
	TotalPatterns      int                  `json:"total_patterns"`
	Reachable          int                  `json:"reachable"`
	WeaklyReachable    int                  `json:"weakly_reachable"`
	Unreachable        int                  `json:"unreachable"`
	UniverseTags       []string             `json:"universe_tags"`
	UnreachablePatterns []ReachabilityResult `json:"unreachable_patterns"`
	WeakPatterns       []ReachabilityResult `json:"weak_patterns"`
}

// RunReachability evaluates every pattern against the achievable tag universe.
//
// A pattern is:
//   - "unreachable" if at least one required tag is not produced by any
//     component, energy source, or keyword-dictionary entry.
//   - "weakly_reachable" if all required tags exist in the universe but
//     no single source (one Component or one EnergySource or one Keyword
//     entry) supplies all of them at once — i.e., it relies on multiple
//     parser hits to combine.
//   - "reachable" if some single source covers all required tags.
//
// The classification ignores ExcludedComponentTags and runtime filters
// (lifecycle/op-state/machine-type), because those are project-level
// concerns. The audit answers "could this pattern EVER fire", not
// "does it fire for project X".
func RunReachability() ReachabilityReport {
	patterns := iace.AllPatterns()
	comps := iace.GetComponentLibrary()
	energies := iace.GetEnergySources()
	keywords := iace.GetKeywordDictionary()

	// Tag universe: union of every tag emitted anywhere
	universe := map[string][]string{} // tag → list of source IDs that emit it
	for _, c := range comps {
		for _, t := range c.Tags {
			universe[t] = appendUnique(universe[t], "component:"+c.ID)
		}
	}
	for _, e := range energies {
		for _, t := range e.Tags {
			universe[t] = appendUnique(universe[t], "energy:"+e.ID)
		}
	}
	for i, kw := range keywords {
		for _, t := range kw.ExtraTags {
			universe[t] = appendUnique(universe[t], keywordLabel(kw, i))
		}
		// Keyword entries can also reference components/energies, which
		// transitively add their tags to the keyword's effective tag set.
		for _, cID := range kw.ComponentIDs {
			for _, c := range comps {
				if c.ID != cID {
					continue
				}
				for _, t := range c.Tags {
					universe[t] = appendUnique(universe[t], keywordLabel(kw, i))
				}
			}
		}
		for _, eID := range kw.EnergyIDs {
			for _, e := range energies {
				if e.ID != eID {
					continue
				}
				for _, t := range e.Tags {
					universe[t] = appendUnique(universe[t], keywordLabel(kw, i))
				}
			}
		}
	}

	// Single-source coverage map: tag → covering sources, but also
	// per-source tag set so we can check "is there ONE source covering
	// all required tags".
	sourceTags := map[string]map[string]bool{}
	for _, c := range comps {
		key := "component:" + c.ID
		sourceTags[key] = toBoolSet(c.Tags)
	}
	for _, e := range energies {
		key := "energy:" + e.ID
		sourceTags[key] = toBoolSet(e.Tags)
	}
	for i, kw := range keywords {
		key := keywordLabel(kw, i)
		set := toBoolSet(kw.ExtraTags)
		for _, cID := range kw.ComponentIDs {
			for _, c := range comps {
				if c.ID == cID {
					for _, t := range c.Tags {
						set[t] = true
					}
				}
			}
		}
		for _, eID := range kw.EnergyIDs {
			for _, e := range energies {
				if e.ID == eID {
					for _, t := range e.Tags {
						set[t] = true
					}
				}
			}
		}
		sourceTags[key] = set
	}

	report := ReachabilityReport{TotalPatterns: len(patterns)}

	// Universe tag list (sorted) for the report header
	for t := range universe {
		report.UniverseTags = append(report.UniverseTags, t)
	}
	sort.Strings(report.UniverseTags)

	for _, p := range patterns {
		all := dedup(append(append([]string{}, p.RequiredComponentTags...), p.RequiredEnergyTags...))
		if len(all) == 0 {
			// Pattern with no tag requirements relies on lifecycle/machine_type
			// filters only — count as reachable by default.
			report.Reachable++
			continue
		}

		var missing []string
		for _, t := range all {
			if _, ok := universe[t]; !ok {
				missing = append(missing, t)
			}
		}

		res := ReachabilityResult{
			PatternID:       p.ID,
			Name:            p.NameDE,
			Priority:        p.Priority,
			RequiredAllTags: all,
		}

		if len(missing) > 0 {
			res.Status = "unreachable"
			res.UnreachableTags = missing
			res.FixSuggestions = suggestFixes(p, missing, comps, sourceTags)
			report.Unreachable++
			report.UnreachablePatterns = append(report.UnreachablePatterns, res)
			continue
		}

		// All tags in universe — check single-source coverage
		single := findSingleSourceCovers(all, sourceTags)
		if len(single) > 0 {
			res.Status = "reachable"
			res.ReachableSources = single
			report.Reachable++
			continue
		}

		res.Status = "weakly_reachable"
		res.FixSuggestions = suggestSingleSourceFixes(p, all, comps, sourceTags)
		report.WeaklyReachable++
		report.WeakPatterns = append(report.WeakPatterns, res)
	}

	sort.Slice(report.UnreachablePatterns, func(i, j int) bool {
		return report.UnreachablePatterns[i].Priority > report.UnreachablePatterns[j].Priority
	})
	sort.Slice(report.WeakPatterns, func(i, j int) bool {
		return report.WeakPatterns[i].Priority > report.WeakPatterns[j].Priority
	})
	return report
}

func findSingleSourceCovers(required []string, sourceTags map[string]map[string]bool) []string {
	var hits []string
	for src, tags := range sourceTags {
		ok := true
		for _, t := range required {
			if !tags[t] {
				ok = false
				break
			}
		}
		if ok {
			hits = append(hits, src)
		}
	}
	sort.Strings(hits)
	return hits
}

// suggestFixes proposes concrete library edits for unreachable patterns:
// "Add tag X to Component C014 (Hubwerk)" type suggestions.
func suggestFixes(p iace.HazardPattern, missing []string, comps []iace.ComponentLibraryEntry, sourceTags map[string]map[string]bool) []string {
	var out []string
	// For each missing tag, find candidates: components/energies that
	// would semantically own that tag based on existing tags overlap.
	for _, tag := range missing {
		candidates := nearComponents(p, tag, comps, sourceTags)
		if len(candidates) > 0 {
			out = append(out, "Add tag '"+tag+"' to one of: "+joinFirst(candidates, 3))
		} else {
			out = append(out, "Tag '"+tag+"' is undefined anywhere — needs a new component or energy source carrying it")
		}
	}
	return out
}

func suggestSingleSourceFixes(p iace.HazardPattern, all []string, comps []iace.ComponentLibraryEntry, sourceTags map[string]map[string]bool) []string {
	// Find components that match the most required tags, then suggest
	// adding the residual ones.
	best := ""
	bestCover := 0
	var bestMissing []string
	for src, tags := range sourceTags {
		hit := 0
		var miss []string
		for _, t := range all {
			if tags[t] {
				hit++
			} else {
				miss = append(miss, t)
			}
		}
		if hit > bestCover {
			best, bestCover, bestMissing = src, hit, miss
		}
	}
	if best == "" || bestCover == 0 {
		return []string{"No single source covers any required tags — pattern needs a new dedicated component"}
	}
	if len(bestMissing) == 0 {
		return nil
	}
	return []string{"Closest single source '" + best + "' covers " + itoa(bestCover) + "/" + itoa(len(all)) + " tags. Add missing tags to it: " + joinFirst(bestMissing, 5)}
}

// nearComponents finds components whose tags overlap most with the pattern's
// requirements — these are good candidates to receive the missing tag.
func nearComponents(p iace.HazardPattern, missing string, comps []iace.ComponentLibraryEntry, sourceTags map[string]map[string]bool) []string {
	required := dedup(append(append([]string{}, p.RequiredComponentTags...), p.RequiredEnergyTags...))
	required = removeOne(required, missing)
	if len(required) == 0 {
		return nil
	}
	type scored struct {
		id    string
		score int
	}
	var scoredList []scored
	for _, c := range comps {
		tagSet := toBoolSet(c.Tags)
		s := 0
		for _, t := range required {
			if tagSet[t] {
				s++
			}
		}
		if s > 0 {
			scoredList = append(scoredList, scored{id: c.ID + " (" + c.NameDE + ")", score: s})
		}
	}
	sort.Slice(scoredList, func(i, j int) bool { return scoredList[i].score > scoredList[j].score })
	var out []string
	for _, s := range scoredList {
		out = append(out, s.id)
	}
	return out
}

func keywordLabel(kw iace.KeywordEntry, idx int) string {
	if len(kw.Keywords) > 0 {
		return "keyword:" + kw.Keywords[0]
	}
	return "keyword:" + itoa(idx)
}
