package executor

import (
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"webby-builder/internal/models"
)

// ApplyDesignOverlay applies a resolved design system onto a freshly-initialized
// template workspace: it writes the accent-agnostic base tokens + the chosen
// accent into src/index.css, injects the font <link>s into index.html, overlays
// any component files (deep systems), and returns the agent playbook (DESIGN.md)
// for prompt injection. Returns ("", nil) when ds is nil/empty (no-op).
func ApplyDesignOverlay(workspacePath string, ds *models.DesignSystem) (playbook string, err error) {
	if ds == nil || ds.Tokens == "" {
		return "", nil
	}

	css := mergeAccentIntoTokens(ds.Tokens, ds.AccentLight, ds.AccentDark)
	if err = os.WriteFile(filepath.Join(workspacePath, "src", "index.css"), []byte(css), 0o644); err != nil {
		return "", err
	}

	if ds.Fonts != "" {
		injectFontsIntoIndexHTML(filepath.Join(workspacePath, "index.html"), ds.Fonts)
	}

	componentsRoot := filepath.Join(workspacePath, "src", "components")
	for rel, content := range ds.Components {
		dest := filepath.Join(workspacePath, "src", filepath.Clean("/"+rel))
		// Defense in depth: only ever write inside src/components/, even if a
		// crafted payload smuggles a "components/../…" entry past the Laravel
		// filter. filepath.Join already confines to the workspace; this confines
		// to the components subtree.
		if dest != componentsRoot && !strings.HasPrefix(dest, componentsRoot+string(os.PathSeparator)) {
			continue
		}
		if mkErr := os.MkdirAll(filepath.Dir(dest), 0o755); mkErr == nil {
			_ = os.WriteFile(dest, []byte(content), 0o644)
		}
	}

	return ds.Playbook, nil
}

// mergeAccentIntoTokens inserts the accent's light vars right after ':root {'
// and the dark vars right after '.dark {' in the accent-agnostic base tokens.
func mergeAccentIntoTokens(tokens string, light, dark map[string]string) string {
	tokens = insertVarsAfter(tokens, ":root {", light)
	tokens = insertVarsAfter(tokens, ".dark {", dark)
	return tokens
}

func insertVarsAfter(css, marker string, vars map[string]string) string {
	i := strings.Index(css, marker)
	if i < 0 || len(vars) == 0 {
		return css
	}
	var b strings.Builder
	for _, k := range accentVarOrder(vars) {
		b.WriteString("\n  --")
		b.WriteString(k)
		b.WriteString(": ")
		b.WriteString(vars[k])
		b.WriteString(";")
	}
	pos := i + len(marker)
	return css[:pos] + b.String() + css[pos:]
}

// accentVarOrder yields a deterministic order: the known accent vars first,
// then any extras sorted — so the merge output is stable.
func accentVarOrder(vars map[string]string) []string {
	known := []string{"primary", "primary-foreground", "ring", "chart-1", "sidebar-primary", "sidebar-primary-foreground", "sidebar-ring"}
	seen := map[string]bool{}
	out := make([]string, 0, len(vars))
	for _, k := range known {
		if _, ok := vars[k]; ok {
			out = append(out, k)
			seen[k] = true
		}
	}
	rest := make([]string, 0)
	for k := range vars {
		if !seen[k] {
			rest = append(rest, k)
		}
	}
	sort.Strings(rest)
	return append(out, rest...)
}

const (
	fontsMarkerOpen  = "<!-- design-system-fonts -->"
	fontsMarkerClose = "<!-- /design-system-fonts -->"
)

// strayFontLinkRe matches any Google Fonts <link> tag (the css2 stylesheet plus
// the fonts.googleapis.com / fonts.gstatic.com preconnect hints) so we can strip
// fonts a template hardcoded outside the marker block.
var strayFontLinkRe = regexp.MustCompile(`(?i)[ \t]*<link[^>]*fonts\.g(?:oogleapis|static)\.com[^>]*>[ \t]*\n?`)

// injectFontsIntoIndexHTML inserts the font <link>s before </head>, wrapped in a
// marker block. Re-theming to a different system REPLACES the block rather than
// appending, and any Google Fonts links a template hardcoded OUTSIDE the marker
// are stripped first — so switching design systems never leaves stale/unused
// font links (and their downloads) behind.
func injectFontsIntoIndexHTML(path, fonts string) {
	data, err := os.ReadFile(path)
	if err != nil {
		return
	}
	html := string(data)

	// 1. Remove any existing design-system-fonts marker block (prior overlay).
	if start := strings.Index(html, fontsMarkerOpen); start >= 0 {
		if end := strings.Index(html[start:], fontsMarkerClose); end >= 0 {
			endAbs := start + end + len(fontsMarkerClose)
			html = html[:start] + html[endAbs:]
		}
	}

	// 2. Strip stray Google Fonts <link>s the template hardcoded outside the
	//    marker block, so a re-theme doesn't keep downloading the old fonts.
	html = strayFontLinkRe.ReplaceAllString(html, "")

	// 3. Inject a fresh marker block before </head>.
	block := fontsMarkerOpen + "\n  " + fonts + "\n  " + fontsMarkerClose
	if idx := strings.Index(html, "</head>"); idx >= 0 {
		html = html[:idx] + block + "\n  " + html[idx:]
	} else {
		html = block + "\n" + html
	}
	_ = os.WriteFile(path, []byte(html), 0o644)
}
