package executor

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

// writeFileAtomic writes data via a temp file + rename so a concurrent reader
// (e.g. the packaging walk during a second simultaneous download) never sees a
// torn or truncated file. Mirrors WriteJSONFileAtomic for raw bytes.
func writeFileAtomic(path string, data []byte) {
	tmp, err := os.CreateTemp(filepath.Dir(path), ".finalize-*.tmp")
	if err != nil {
		return
	}
	tmpName := tmp.Name()
	if _, wErr := tmp.Write(data); wErr != nil || tmp.Sync() != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		return
	}
	if cErr := tmp.Close(); cErr != nil {
		_ = os.Remove(tmpName)
		return
	}
	_ = os.Rename(tmpName, path)
}

// FinalizeWordPressTheme makes a packaged theme distribution-grade without
// touching anything the AI or user authored: registers unregistered template
// parts in theme.json, completes missing style.css header fields, and
// generates readme.txt + screenshot.png when absent. Idempotent and
// best-effort — every sub-step degrades independently; it never fails the
// download. Called by the theme packaging endpoint right before zipping.
func FinalizeWordPressTheme(workspacePath string) {
	syncTemplateParts(workspacePath)
	completeStyleHeader(workspacePath)
	ensureReadme(workspacePath)
	ensureScreenshot(workspacePath)
}

var styleHeaderFieldRe = regexp.MustCompile(`(?m)^\s*([A-Za-z ]+?):\s*(.*)$`)

// parseStyleHeader returns the key→value fields of the first /* … */ comment
// in style.css plus the raw file content. ok=false when no header exists.
func parseStyleHeader(workspacePath string) (fields map[string]string, raw string, ok bool) {
	data, err := os.ReadFile(filepath.Join(workspacePath, "style.css"))
	if err != nil {
		return nil, "", false
	}
	raw = string(data)
	start := strings.Index(raw, "/*")
	end := strings.Index(raw, "*/")
	if start < 0 || end < start {
		return nil, raw, false
	}
	fields = map[string]string{}
	for _, m := range styleHeaderFieldRe.FindAllStringSubmatch(raw[start:end], -1) {
		fields[strings.TrimSpace(m[1])] = strings.TrimSpace(m[2])
	}
	return fields, raw, true
}

var nonSlugRe = regexp.MustCompile(`[^a-z0-9]+`)

func slugifyThemeName(name string) string {
	s := nonSlugRe.ReplaceAllString(strings.ToLower(name), "-")
	return strings.Trim(s, "-")
}

// completeStyleHeader appends required-but-missing header fields before the
// closing */ of the first comment. Existing fields are never modified.
func completeStyleHeader(workspacePath string) {
	fields, raw, ok := parseStyleHeader(workspacePath)
	if !ok {
		return
	}
	slug := fields["Text Domain"]
	if slug == "" {
		slug = slugifyThemeName(fields["Theme Name"])
	}
	if slug == "" {
		slug = "block-theme"
	}
	defaults := []struct{ key, value string }{
		{"Version", "1.0.0"},
		{"Requires at least", WPMinVersion},
		{"Tested up to", WPTargetVersion},
		{"Requires PHP", "7.4"},
		{"License", "GPL-2.0-or-later"},
		{"License URI", "https://www.gnu.org/licenses/gpl-2.0.html"},
		{"Text Domain", slug},
		{"Tags", "full-site-editing, block-patterns, block-styles"},
	}
	var missing []string
	for _, d := range defaults {
		if _, has := fields[d.key]; !has {
			missing = append(missing, d.key+": "+d.value)
		}
	}
	if len(missing) == 0 {
		return
	}
	end := strings.Index(raw, "*/")
	prefix := raw[:end]
	if !strings.HasSuffix(prefix, "\n") {
		// Single-line headers (/* Theme Name: X */) need a break before the
		// appended fields or they'd run onto the existing line.
		prefix += "\n"
	}
	updated := prefix + strings.Join(missing, "\n") + "\n" + raw[end:]
	writeFileAtomic(filepath.Join(workspacePath, "style.css"), []byte(updated))
}

// syncTemplateParts registers every parts/*.html file in theme.json's
// templateParts (header*/footer* name heuristics for the area). Existing
// registrations are preserved verbatim.
func syncTemplateParts(workspacePath string) {
	entries, err := os.ReadDir(filepath.Join(workspacePath, "parts"))
	if err != nil {
		return
	}
	themePath := filepath.Join(workspacePath, "theme.json")
	theme, err := ReadJSONFile(themePath)
	if err != nil {
		return
	}
	existing, _ := theme["templateParts"].([]interface{})
	registered := map[string]bool{}
	for _, e := range existing {
		if m, isMap := e.(map[string]interface{}); isMap {
			if name, _ := m["name"].(string); name != "" {
				registered[name] = true
			}
		}
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".html") {
			continue
		}
		names = append(names, strings.TrimSuffix(e.Name(), ".html"))
	}
	sort.Strings(names)
	changed := false
	for _, name := range names {
		if registered[name] {
			continue
		}
		area := "uncategorized"
		switch {
		case strings.HasPrefix(name, "header"):
			area = "header"
		case strings.HasPrefix(name, "footer"):
			area = "footer"
		}
		existing = append(existing, map[string]interface{}{
			"name": name, "area": area, "title": capitalize(name),
		})
		changed = true
	}
	if changed {
		theme["templateParts"] = existing
		_ = WriteJSONFileAtomic(themePath, theme)
	}
}

// ensureReadme writes a minimal directory-shaped readme.txt when none exists.
func ensureReadme(workspacePath string) {
	path := filepath.Join(workspacePath, "readme.txt")
	if _, err := os.Stat(path); err == nil {
		return
	}
	fields, _, ok := parseStyleHeader(workspacePath)
	if !ok {
		return
	}
	name := fields["Theme Name"]
	if name == "" {
		name = "Block Theme"
	}
	desc := fields["Description"]
	if desc == "" {
		desc = "A WordPress block theme."
	}
	version := fields["Version"]
	if version == "" {
		version = "1.0.0"
	}
	var b strings.Builder
	b.WriteString("=== " + name + " ===\n")
	b.WriteString("Requires at least: " + WPMinVersion + "\n")
	b.WriteString("Tested up to: " + WPTargetVersion + "\n")
	b.WriteString("Requires PHP: 7.4\n")
	b.WriteString("Stable tag: " + version + "\n")
	b.WriteString("License: GPL-2.0-or-later\n")
	b.WriteString("License URI: https://www.gnu.org/licenses/gpl-2.0.html\n\n")
	b.WriteString(desc + "\n\n")
	b.WriteString("== Description ==\n\n" + desc + "\n\n")
	if entries, fErr := os.ReadDir(filepath.Join(workspacePath, "assets", "fonts")); fErr == nil && len(entries) > 0 {
		b.WriteString("== Fonts ==\n\nBundled fonts are licensed under the SIL Open Font License (OFL).\n\n")
	}
	b.WriteString("== Changelog ==\n\n= " + version + " =\n* Initial release.\n")
	writeFileAtomic(path, []byte(b.String()))
}

// ensureScreenshot generates the branding-card screenshot when none exists,
// using the workspace's own theme.json palette + style.css theme name.
func ensureScreenshot(workspacePath string) {
	path := filepath.Join(workspacePath, "screenshot.png")
	if _, err := os.Stat(path); err == nil {
		return
	}
	fields, _, _ := parseStyleHeader(workspacePath)
	name := fields["Theme Name"]
	palette := map[string]string{}
	if theme, err := ReadJSONFile(filepath.Join(workspacePath, "theme.json")); err == nil {
		if settings, sOK := theme["settings"].(map[string]interface{}); sOK {
			if colorS, cOK := settings["color"].(map[string]interface{}); cOK {
				if entries, pOK := colorS["palette"].([]interface{}); pOK {
					for _, e := range entries {
						if m, mOK := e.(map[string]interface{}); mOK {
							slug, _ := m["slug"].(string)
							col, _ := m["color"].(string)
							if slug != "" && col != "" {
								palette[slug] = col
							}
						}
					}
				}
			}
		}
	}
	_ = GenerateThemeScreenshot(path, name, palette)
}
