// Package sandbox — unicode_scan.go
//
//
// Defends against GlassWorm (variation selector payloads), Trojan Source
// (bidi override attacks / CVE-2021-42574), and other invisible Unicode
// smuggling techniques.
//
// Two public functions:
//   ScanSource(source, filename) → []Finding
//   Verdict(findings)            → (blocked bool, reason string)
//
// No dependencies beyond stdlib (unicode, fmt, strings).
// Single-pass rune iteration. No regex.
package sandbox

import (
	"fmt"
	"strings"
	"unicode"
)

// invisibleRanges defines the Unicode ranges we scan for, grouped by category.
// Uses unicode.RangeTable with R16/R32 slices for efficient lookup.
var invisibleRanges = map[string]*unicode.RangeTable{
	"zero_width": {
		R16: []unicode.Range16{
			{Lo: 0x200B, Hi: 0x200F, Stride: 1}, // zero-width space, joiners, bidi marks
			{Lo: 0x2060, Hi: 0x2064, Stride: 1}, // word joiner, invisible operators
			{Lo: 0xFEFF, Hi: 0xFEFF, Stride: 1}, // BOM / zero-width no-break space
		},
	},
	"bidi_override": {
		R16: []unicode.Range16{
			{Lo: 0x202A, Hi: 0x202E, Stride: 1}, // bidi overrides (Trojan Source)
			{Lo: 0x2066, Hi: 0x2069, Stride: 1}, // bidi isolates
		},
	},
	"variation_selector": {
		R16: []unicode.Range16{
			{Lo: 0xFE00, Hi: 0xFE0F, Stride: 1}, // variation selectors (GlassWorm primary)
		},
		R32: []unicode.Range32{
			{Lo: 0xE0100, Hi: 0xE01EF, Stride: 1}, // variation selector supplement (GlassWorm secondary)
		},
	},
	"tag_character": {
		R32: []unicode.Range32{
			{Lo: 0xE0001, Hi: 0xE007F, Stride: 1}, // tag characters
		},
	},
	"other_invisible": {
		R16: []unicode.Range16{
			{Lo: 0x3164, Hi: 0x3164, Stride: 1}, // Hangul filler
			{Lo: 0xFFF9, Hi: 0xFFFB, Stride: 1}, // interlinear annotations
		},
	},
}

// Finding records a single invisible/deceptive Unicode character found in source.
type Finding struct {
	Offset   int    // byte offset in source
	Line     int    // 1-based line number
	Rune     rune   // the character found
	Category string // one of: variation_selector, bidi_override, zero_width, tag_character, other_invisible
}

// ScanSource scans source code for invisible/deceptive Unicode characters.
// Returns a slice of findings (empty if clean). Single-pass rune iteration.
func ScanSource(source string, filename string) []Finding {
	var findings []Finding
	line := 1
	offset := 0

	for _, r := range source {
		for cat, rt := range invisibleRanges {
			if unicode.Is(rt, r) {
				findings = append(findings, Finding{
					Offset:   offset,
					Line:     line,
					Rune:     r,
					Category: cat,
				})
				break
			}
		}
		if r == '\n' {
			line++
		}
		offset += len(string(r))
	}

	return findings
}

// CategoryCounts returns a map of category → count from a slice of findings.
func CategoryCounts(findings []Finding) map[string]int {
	counts := make(map[string]int)
	for _, f := range findings {
		counts[f.Category]++
	}
	return counts
}

// Verdict evaluates findings and returns whether the source should be blocked.
//
// Blocking rules:
//   - Any bidi_override → block (Trojan Source, zero legitimate use in Starlark)
//   - variation_selector + tag_character >= 10 → block (GlassWorm signature)
//   - zero_width >= 6 → block
//
// Returns blocked=false, reason="" if clean or below threshold.
func Verdict(findings []Finding) (blocked bool, reason string) {
	if len(findings) == 0 {
		return false, ""
	}

	counts := CategoryCounts(findings)

	// Rule 1: Any bidi override → always block (Trojan Source / CVE-2021-42574)
	if counts["bidi_override"] > 0 {
		return true, fmt.Sprintf(
			"blocked: %d bidirectional override character(s) detected (Trojan Source / CVE-2021-42574). "+
				"These characters can reverse the visual display order of code, hiding malicious logic.",
			counts["bidi_override"],
		)
	}

	// Rule 2: GlassWorm signature — variation selectors + tag characters >= 10
	glasswormCount := counts["variation_selector"] + counts["tag_character"]
	if glasswormCount >= 10 {
		return true, fmt.Sprintf(
			"blocked: %d variation selector / tag characters detected (GlassWorm pattern). "+
				"Legitimate emoji use requires 1-2; %d suggests encoded hidden payload.",
			glasswormCount, glasswormCount,
		)
	}

	// Rule 3: Excessive zero-width characters
	if counts["zero_width"] >= 6 {
		return true, fmt.Sprintf(
			"blocked: %d zero-width characters detected. "+
				"This exceeds the safe threshold and may indicate hidden content.",
			counts["zero_width"],
		)
	}

	// Below threshold — not blocked
	var parts []string
	for cat, n := range counts {
		parts = append(parts, fmt.Sprintf("%d %s", n, cat))
	}
	return false, strings.Join(parts, ", ")
}
