package ucca

import (
	"regexp"
	"strconv"
	"strings"
)

// authorityInfo is the normative classification of a search result, used internally
// for re-ranking only (Phase 1 changes ordering, not the response contract).
type authorityInfo struct {
	weight       int    // 100 binding, 80 technical_standard, 70 guidance, 0 foreign, 50 unknown
	sourceClass  string // binding_law | technical_standard | supervisory_guidance | foreign_law | unknown
	jurisdiction string // DE | EU | CH
}

var (
	guidanceMarkers = []string{
		"DSK", "EDPB", "BfDI", "BFDI", "BayLfD", "Baylfb", "ENISA", "BSI", "EUCC",
		"Standards Mapping", "Kpnr", "Orientierungshilfe", "Handreichung", "Beschluss",
		"Leitlinie", "Guidance", "Empfehlung", "OECD", "CISA", "Blue Guide",
	}
	// Technical standards / control frameworks (best-practice controls). Checked BEFORE
	// guidanceMarkers so a "BSI Grundschutz" chunk classifies as a standard, not BSI guidance.
	standardMarkers = []string{
		"NIST", "OWASP", "Grundschutz", "ISO 27001", "ISO/IEC 27001",
		"CSA CCM", "Cloud Controls Matrix", "CIS Benchmark", "CIS Control",
	}
	foreignMarkers = []string{"RevDSG", "fedlex", "(CH)"}
	deMarkers      = []string{"BDSG", "DSK", "BfDI", "BFDI", "BayLfD", "Baylfb", "BSI"}
	normPattern    = regexp.MustCompile(`(§|Art\.?)\s*\d`)
	bdsgParagraph  = regexp.MustCompile(`§\s*(\d+)`)
)

// classifyAuthority derives weight/source-class/jurisdiction. Explicitly tagged payload
// values win; otherwise it falls back to the curated category + name markers, so the
// not-yet-re-ingested (untagged) corpus is still classified deterministically.
func classifyAuthority(r LegalSearchResult) authorityInfo {
	jur := r.Jurisdiction
	if jur == "" {
		jur = inferJurisdiction(r)
	}
	hay := r.ArticleLabel + " " + r.RegulationShort + " " + r.RegulationName + " " + r.RegulationCode
	// A recognised standard NAME (NIST/OWASP/ISO 27001/CIS/CSA CCM/Grundschutz) is authoritative
	// even when the corpus mis-tagged the chunk as supervisory_guidance (weight 70) — many
	// standards were ingested with a generic guidance source_class. The name wins, so they
	// classify (and rank) as technical_standard / control_standard. binding_law is preserved.
	if r.SourceClass != "binding_law" && containsAny(hay, standardMarkers) {
		return authorityInfo{weight: 80, sourceClass: "technical_standard", jurisdiction: jur}
	}
	if r.SourceClass != "" {
		w := r.AuthorityWeight
		if w == 0 && r.SourceClass == "binding_law" {
			w = 100
		}
		return authorityInfo{weight: w, sourceClass: r.SourceClass, jurisdiction: jur}
	}
	if r.AuthorityWeight > 0 {
		return authorityInfo{weight: r.AuthorityWeight, sourceClass: sourceClassFromWeight(r.AuthorityWeight), jurisdiction: jur}
	}
	switch {
	case containsAny(hay, foreignMarkers):
		return authorityInfo{weight: 0, sourceClass: "foreign_law", jurisdiction: "CH"}
	case r.Category == "standard" || containsAny(hay, standardMarkers):
		return authorityInfo{weight: 80, sourceClass: "technical_standard", jurisdiction: jur}
	case r.Category == "guidance" || containsAny(hay, guidanceMarkers):
		return authorityInfo{weight: 70, sourceClass: "supervisory_guidance", jurisdiction: jur}
	case r.Category == "regulation" || r.Category == "eu_recht" || normPattern.MatchString(r.ArticleLabel):
		return authorityInfo{weight: 100, sourceClass: "binding_law", jurisdiction: jur}
	default:
		return authorityInfo{weight: 50, sourceClass: "unknown", jurisdiction: jur}
	}
}

func sourceClassFromWeight(w int) string {
	switch {
	case w >= 100:
		return "binding_law"
	case w >= 80:
		return "technical_standard"
	case w >= 70:
		return "supervisory_guidance"
	case w <= 0:
		return "foreign_law"
	default:
		return "unknown"
	}
}

func inferJurisdiction(r LegalSearchResult) string {
	hay := r.ArticleLabel + " " + r.RegulationShort + " " + r.RegulationName
	switch {
	case containsAny(hay, foreignMarkers):
		return "CH"
	case strings.Contains(hay, "§") || containsAny(hay, deMarkers):
		return "DE"
	default:
		return "EU"
	}
}

// --- Domain routing: separates same-authority but topically foreign norms ---

type domainDef struct {
	name     string
	regs     []string // regulation markers found in a chunk
	keywords []string // query keywords that signal this domain
}

// Deterministic order (slice, not map) — important for stable classification + tests.
var domains = []domainDef{
	{"data_protection",
		[]string{"DSGVO", "GDPR", "BDSG", "TDDDG", "TTDSG", "EDPB", "DSK", "BfDI", "BayLfD", "DPF"},
		[]string{"personenbezogen", "betroffene", "datenschutz", "datenschutzbeauftrag", "dsb",
			"datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeit",
			"cookie", "endeinrichtung", "endgerät", "endgeraet", "tracking"}},
	{"cyber",
		[]string{"CRA", "NIS2", "NIS-2", "ENISA", "DORA", "EUCC"},
		[]string{"security update", "sicherheitsupdate", "sicherheitsaktualisierung", "schwachstelle", "sbom",
			"cybersicherheit", "konformit", "hersteller", "importeur", "haendler", "händler", "ikt-",
			"resilienz", "sicherheitsvorfall", "digitalen elementen"}},
	{"ai",
		[]string{"AI Act", "KI-VO", "KI-Verordnung"},
		[]string{"ki-system", "ki-modell", "hochrisiko", "kuenstliche intelligenz", "künstliche intelligenz"}},
	{"product_safety",
		[]string{"Maschinenverordnung", "MaschinenVO", "GPSR", "RED", "MDR"},
		nil},
}

// euPrimaryDomains are domains whose PRIMARY binding act is an EU regulation/directive
// (DSGVO, CRA/NIS2, AI Act, MaschinenVO). In these domains a NATIONAL implementing law
// (e.g. BDSG) is subsidiary for general questions — see nationalSubsidiarityPenalty.
var euPrimaryDomains = map[string]bool{
	"data_protection": true,
	"cyber":           true,
	"ai":              true,
	"product_safety":  true,
}

func queryDomain(query string) string {
	ql := strings.ToLower(query)
	for _, d := range domains {
		for _, kw := range d.keywords {
			if strings.Contains(ql, kw) {
				return d.name
			}
		}
	}
	// Fallback: an explicit regulation mention (e.g. "DSGVO", "BDSG", "CRA") also signals the
	// domain — so a question phrased around the act ("... gilt die DSGVO ...") is scoped even
	// without a topical keyword. Keyword match wins first (more specific).
	for _, d := range domains {
		for _, reg := range d.regs {
			if strings.Contains(ql, strings.ToLower(reg)) {
				return d.name
			}
		}
	}
	return ""
}

func chunkDomain(r LegalSearchResult) string {
	hay := r.ArticleLabel + " " + r.RegulationShort + " " + r.RegulationCode + " " + r.RegulationName
	for _, d := range domains {
		if containsAny(hay, d.regs) {
			return d.name
		}
	}
	return ""
}

// scopeClass flags special sub-regimes that must not win general questions —
// BDSG Teil 3 (§§ 45-84) implements the JI directive (law enforcement), not the general regime.
func scopeClass(r LegalSearchResult) string {
	hay := r.ArticleLabel + " " + r.RegulationShort
	if strings.Contains(hay, "BDSG") {
		if m := bdsgParagraph.FindStringSubmatch(hay); m != nil {
			if n, err := strconv.Atoi(m[1]); err == nil && n >= 45 && n <= 84 {
				return "law_enforcement"
			}
		}
	}
	return "general"
}

// --- Topic ontology: amplifier only (boost), never an override ---

type topicDef struct {
	keywords []string
	norms    []string // preferred canonical citation fragments
}

var topics = []topicDef{
	{[]string{"datenschutzbeauftrag", "dsb", "benennung"}, []string{"Art. 37", "§ 38 BDSG"}},
	{[]string{"stellung des"}, []string{"Art. 38"}},
	{[]string{"aufgaben des"}, []string{"Art. 39"}},
	{[]string{"folgenabsch", "dsfa"}, []string{"Art. 35"}},
	{[]string{"besondere kategorien"}, []string{"Art. 9", "§ 22 BDSG"}},
	{[]string{"auskunft"}, []string{"Art. 15", "§ 34 BDSG"}},
	{[]string{"loesch", "lösch"}, []string{"Art. 17", "§ 35 BDSG"}},
	{[]string{"bussgeld", "geldbusse"}, []string{"Art. 83"}},
	{[]string{"security update", "sicherheitsupdate", "schwachstelle", "sbom", "cybersicherheitsanforderung"}, []string{"CRA Anhang I"}},
	{[]string{"meldepflicht", "sicherheitsvorfall"}, []string{"Art. 14 CRA"}},
	// ePrivacy / cookies: § 25 TDDDG (ex-TTDSG) is lex specialis for terminal-equipment access /
	// cookie consent. Co-primary on a cookie/tracking query, so the subsidiarity rule does NOT
	// demote it like general-DP DE law subsidiary to the DSGVO. Keywords are cookie-specific
	// (NOT bare "Einwilligung") so a general consent question still resolves to Art. 7 DSGVO.
	{[]string{"cookie", "endeinrichtung", "endgerät", "endgeraet", "tracking", "speicherung von informationen", "zugriff auf informationen"}, []string{"§ 25 TDDDG"}},
}

// resultMatchesTopic reports whether the result is a preferred norm of a topic the query hits.
func resultMatchesTopic(query string, r LegalSearchResult) bool {
	ql := strings.ToLower(query)
	hay := r.ArticleLabel + " " + r.RegulationShort
	for _, t := range topics {
		if !containsAnyLower(ql, t.keywords) {
			continue
		}
		for _, n := range t.norms {
			if normMatches(hay, n) {
				return true
			}
		}
	}
	return false
}

// normMatches checks that norm appears in hay with a non-digit boundary, so "Art. 9"
// matches "Art. 9 DSGVO" but not "Art. 90".
func normMatches(hay, norm string) bool {
	idx := strings.Index(hay, norm)
	if idx < 0 {
		return false
	}
	end := idx + len(norm)
	if end < len(hay) && hay[end] >= '0' && hay[end] <= '9' {
		return false
	}
	return true
}

func queryIsForeign(query string) bool {
	return containsAnyLower(strings.ToLower(query),
		[]string{"schweiz", "revdsg", "fedlex", " ch ", "oesterreich", "österreich"})
}

func containsAny(hay string, markers []string) bool {
	for _, m := range markers {
		if strings.Contains(hay, m) {
			return true
		}
	}
	return false
}

func containsAnyLower(haylower string, markers []string) bool {
	for _, m := range markers {
		if strings.Contains(haylower, strings.ToLower(m)) {
			return true
		}
	}
	return false
}
