// Command aria-check is a static accessibility linter for kjol front-ends. It reads
// source (Solid .tsx and gowasm .go alike — anything that authors Tailwind classes as
// string literals) and reports problems that can be caught without a browser.
//
// The first and only check today is COLOUR CONTRAST, implemented against the same
// algorithm as WebAIM's contrast checker (https://webaim.org/resources/contrastchecker/):
// WCAG 2.x relative luminance in sRGB, ratio (L1+0.05)/(L2+0.05).
//
// How it works:
//
//  1. Scan sources for string literals that hold class lists, keeping the utilities
//     that co-occur in one literal together (contrast is about a foreground and a
//     background on the SAME element — see scan.go).
//  2. Compile every bg-/text- token through kjol's own Tailwind engine (package tw)
//     and read back the colours it resolves — palette OKLCH, hex tokens, semantic
//     tokens, opacity modifiers and arbitrary values all included (theme.go).
//  3. For each co-occurring foreground/background pair, in both the light and dark
//     appearances, composite away any translucency and measure the ratio against the
//     WCAG threshold (check.go, color.go).
//
// Because the tokens are resolved by the real engine, aria-check stays correct as the
// design system changes: it never hard-codes a colour.
//
// Usage (globs/roots are relative to -base; omit them to scan the whole base):
//
//	aria-check -base . -entry app/style.css frontend
//	aria-check -base kjol/go/webui -level AAA
//	aria-check -assume-surface -json .
//
// Exit status is non-zero when any contrast failure is found (unless -warn), so it
// drops straight into CI or a pre-commit hook.
//
// Extending it: contrast is one Checker; the scan + resolve scaffolding is meant to
// carry more. Natural next checks that are equally static-friendly — missing alt text
// on images, controls with no accessible label, heading-order jumps, redundant/absent
// ARIA roles — would each add a pass over the same file walk. See the closing notes in
// the repository discussion for the full list.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

func main() {
	var (
		base          = flag.String("base", ".", "root directory to scan and resolve -entry against")
		entry         = flag.String("entry", "", "app brand Tailwind entry stylesheet (optional; kjol's theme layer is always included)")
		level         = flag.String("level", "AA", "WCAG conformance level: AA or AAA")
		min           = flag.Float64("min", 0, "override the required ratio for normal-size text (AA only)")
		exts          = flag.String("ext", strings.Join(DefaultExts, ","), "comma-separated source file extensions to scan")
		assumeSurface = flag.Bool("assume-surface", false, "also check text-only elements against the page surface colour")
		asJSON        = flag.Bool("json", false, "emit findings as JSON")
		verbose       = flag.Bool("v", false, "also report passing pairs")
		warn          = flag.Bool("warn", false, "always exit 0, even when failures are found")
	)
	flag.Parse()

	opt := Options{Level: *level, MinOverride: *min, AssumeSurface: *assumeSurface}

	groups, err := ScanTree(*base, flag.Args(), splitExts(*exts))
	if err != nil {
		fatal(err)
	}

	var entryCSS string
	if *entry != "" {
		b, err := os.ReadFile(*entry)
		if err != nil {
			fatal(err)
		}
		entryCSS = string(b)
	}

	resolver, err := NewResolver(entryCSS, *base, ColorCandidates(groups))
	if err != nil {
		fatal(fmt.Errorf("compiling theme: %w", err))
	}

	findings := Check(groups, resolver, opt)
	sort.Slice(findings, func(i, j int) bool {
		if findings[i].File != findings[j].File {
			return findings[i].File < findings[j].File
		}
		if findings[i].Line != findings[j].Line {
			return findings[i].Line < findings[j].Line
		}
		return findings[i].Ratio < findings[j].Ratio
	})

	failures := 0
	for _, f := range findings {
		if !f.Pass {
			failures++
		}
	}

	if *asJSON {
		reportJSON(findings, *verbose)
	} else {
		reportText(findings, failures, len(groups), *verbose)
	}

	if failures > 0 && !*warn {
		os.Exit(1)
	}
}

func reportText(findings []Finding, failures, groups int, verbose bool) {
	filesWithFail := map[string]bool{}
	for _, f := range findings {
		if f.Pass && !verbose {
			continue
		}
		if !f.Pass {
			filesWithFail[f.File] = true
		}
		fmt.Println(formatFinding(f))
	}

	fmt.Println()
	if failures == 0 {
		fmt.Printf("aria-check: no contrast failures (%d class groups scanned)\n", groups)
		return
	}
	fmt.Printf("aria-check: %d contrast failure(s) across %d file(s) — %d class groups scanned\n",
		failures, len(filesWithFail), groups)
}

func formatFinding(f Finding) string {
	verdict := "FAIL"
	if f.Pass {
		verdict = "ok  "
	}
	size := "normal"
	if f.Large {
		size = "large"
	}
	bg := f.BG
	if bg == "" {
		bg = "surface"
	}
	rel, err := filepath.Rel(".", f.File)
	if err != nil {
		rel = f.File
	}
	return fmt.Sprintf("%s:%d: %s %s %.2f:1 (need %.1f:1)  %s on %s  [%s %s]\n    %s → %s   %q",
		rel, f.Line, verdict, f.ThemeName(), f.Ratio, f.Required,
		f.FG, bg, f.ThemeName(), size,
		hexOf(f.FGColor), hexOf(f.BGColor), truncate(f.Snip, 90))
}

type jsonFinding struct {
	File     string  `json:"file"`
	Line     int     `json:"line"`
	Theme    string  `json:"theme"`
	FG       string  `json:"fg"`
	BG       string  `json:"bg"`
	FGColor  string  `json:"fgColor"`
	BGColor  string  `json:"bgColor"`
	Ratio    float64 `json:"ratio"`
	Required float64 `json:"required"`
	Large    bool    `json:"large"`
	Pass     bool    `json:"pass"`
}

func reportJSON(findings []Finding, verbose bool) {
	out := make([]jsonFinding, 0, len(findings))
	for _, f := range findings {
		if f.Pass && !verbose {
			continue
		}
		bg := f.BG
		if bg == "" {
			bg = "surface"
		}
		out = append(out, jsonFinding{
			File: f.File, Line: f.Line, Theme: f.ThemeName(),
			FG: f.FG, BG: bg, FGColor: hexOf(f.FGColor), BGColor: hexOf(f.BGColor),
			Ratio: round2(f.Ratio), Required: f.Required, Large: f.Large, Pass: f.Pass,
		})
	}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	_ = enc.Encode(out)
}

func hexOf(c RGBA) string {
	to := func(v float64) int { return int(clamp01(v)*255 + 0.5) }
	if c.A < 1 {
		return fmt.Sprintf("#%02x%02x%02x%02x", to(c.R), to(c.G), to(c.B), to(c.A))
	}
	return fmt.Sprintf("#%02x%02x%02x", to(c.R), to(c.G), to(c.B))
}

func round2(v float64) float64 { return float64(int(v*100+0.5)) / 100 }

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n-1] + "…"
}

func splitExts(s string) []string {
	var out []string
	for _, e := range strings.Split(s, ",") {
		e = strings.TrimSpace(e)
		if e == "" {
			continue
		}
		if !strings.HasPrefix(e, ".") {
			e = "." + e
		}
		out = append(out, strings.ToLower(e))
	}
	return out
}

func fatal(err error) {
	fmt.Fprintln(os.Stderr, "aria-check:", err)
	os.Exit(2)
}
