package bundler

// CSS pipeline: compiles the Tailwind entry stylesheet (frontend/css/style.css)
// with the native Go Tailwind v4 engine (tailwind.go — twCompile/scanSources, no
// goja), feeding it the utility-class candidates scanned from source files, then
// minifies via tdewolff/minify.  style.css is the single entry/config for both
// bundles — they differ only in which source files are scanned for candidates.

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/tdewolff/minify/v2"
	mincss "github.com/tdewolff/minify/v2/css"
)

// Tailwind source patterns - configured here rather than in CSS so each bundle
// scans only the files it actually needs.  The scanner filters by the suffix of
// each pattern, so separate entries are needed for .js and .ts source files.
var twSourcesSPA = []string{
	"../src/**/*.js",
	"../src/**/*.ts",
	"../src/**/*.jsx",
	"../src/**/*.tsx",
	"../../internal/handlers/templates/**/*.html",
}
var twSourcesPublic = []string{
	"../../internal/handlers/templates/**/*.html",
	// Public pages authored as Solid components in .tsx (SSR'd via goja).
	"../src/pages/public/**/*.tsx",
	"../src/pages/public/**/*.jsx",
	"../src/pages/public/**/*.ts",
	"../src/pages/public/**/*.js",
	// Shared UI component library. If a public page renders any ui/ component
	// (data tables, tabs, icons, etc.), its classes must be in this bundle too,
	// so scan the whole library rather than just the env badge / layout.
	"../src/ui/**/*.ts",
	"../src/ui/**/*.js",
}

// styleEntry is the Tailwind entry/config, relative to frontendDir.  Both the SPA
// and public bundles compile it; include.css (a former one-line passthrough) is gone.
const styleEntry = "css/style.css"

var m *minify.M

func init() {
	m = minify.New()
	m.AddFunc("text/css", mincss.Minify)
}

func bundleSPACSS() (bundleStats, error) {
	return compileCSSBundle("SPA", twSourcesSPA, "bundle.min.css")
}

func bundlePublicCSS() (bundleStats, error) {
	return compileCSSBundle("public", twSourcesPublic, "public.bundle.min.css")
}

// compileCSSBundle scans twSources for candidates, compiles style.css with the
// official Tailwind compiler, minifies, and writes outName to wwwroot.  It prints
// a timing line for the Tailwind step so the compile can be profiled.
func compileCSSBundle(label string, twSources []string, outName string) (bundleStats, error) {
	entryPath := filepath.Join(frontendDir, styleEntry)
	src, err := os.ReadFile(entryPath)
	if err != nil {
		return bundleStats{}, fmt.Errorf("reading %s: %w", styleEntry, err)
	}
	cssDir := filepath.Dir(entryPath)

	// Candidate classes come from the app sources (patterns relative to the css
	// dir) plus the shared kit tree scanned directly, so kit component classes are
	// present even though the kit lives outside the app frontend.
	candidates := scanSources(cssDir, twSources)
	kitCands := scanSources(kitSrcDir(), []string{"**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"})
	candidates = dedupStrings(append(candidates, kitCands...))

	// Prepend the shared @theme scaffold (kjol web/styles/theme.css) ahead of the
	// app's brand style.css so its tokens/vars are in scope. Absent in single-tree
	// mode (the app's style.css is already complete).
	input := string(src)
	if tp := themeCSSPath(); tp != "" {
		if theme, e := os.ReadFile(tp); e == nil {
			input = string(theme) + "\n" + input
		}
	}

	compiled, count, err := twCompile(input, cssDir, candidates)
	if err != nil {
		return bundleStats{}, fmt.Errorf("tailwind compile (%s): %w", label, err)
	}
	fmt.Printf("  Tailwind (%s): %d candidates, %d utilities compiled\n",
		label, len(candidates), count)

	minified, err := m.String("text/css", compiled)
	if err != nil {
		return bundleStats{}, fmt.Errorf("minifying %s CSS: %w", label, err)
	}

	outPath := filepath.Join(outputDir, outName)
	if err := os.WriteFile(outPath, []byte(minified), 0644); err != nil {
		return bundleStats{}, err
	}
	return bundleStats{files: len(candidates), bytes: len(minified)}, nil
}

// dedupStrings returns in with duplicates removed, preserving first-seen order.
func dedupStrings(in []string) []string {
	seen := make(map[string]bool, len(in))
	out := make([]string, 0, len(in))
	for _, s := range in {
		if !seen[s] {
			seen[s] = true
			out = append(out, s)
		}
	}
	return out
}
