package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/go-chi/chi/v5"

	"qctextbuilder/internal/buildsvc"
	"qctextbuilder/internal/config"
	"qctextbuilder/internal/domain"
	"qctextbuilder/internal/draftsvc"
	"qctextbuilder/internal/llmruntime"
	"qctextbuilder/internal/mapping"
	"qctextbuilder/internal/onboarding"
	"qctextbuilder/internal/store"
	"qctextbuilder/internal/templatesvc"
)

type UI struct {
	templateSvc         *templatesvc.Service
	onboardSvc          *onboarding.Service
	draftSvc            *draftsvc.Service
	buildSvc            buildsvc.Service
	settings            store.SettingsStore
	suggestionGenerator mapping.SuggestionGenerator
	cfg                 config.Config
	render              htmlRenderer
	logger              *slog.Logger
}

type htmlRenderer interface {
	Render(w http.ResponseWriter, name string, data any)
}

type pageData struct {
	Title   string
	Msg     string
	Err     string
	Current string
}

type homePageData struct {
	pageData
	TemplateCount int
}

type settingsPageData struct {
	pageData
	QCBaseURL              string
	PollIntervalSeconds    int
	PollTimeoutSeconds     int
	PollMaxConcurrent      int
	TokenConfigured        bool
	LanguageOutputMode     string
	LLMProviderOptions     []domain.LLMProviderOption
	LLMModelOptions        []domain.LLMModelOption
	LLMActiveProvider      string
	LLMActiveModel         string
	LLMBaseURL             string
	LLMTemperature         float64
	LLMMaxTokens           int
	OpenAIKeyConfigured    bool
	AnthropicKeyConfigured bool
	GoogleKeyConfigured    bool
	XAIKeyConfigured       bool
	OllamaKeyConfigured    bool
	MasterPrompt           string
	PromptBlocks           []domain.PromptBlockConfig
}

type templatesPageData struct {
	pageData
	Templates []domain.Template
}

type templateFieldView struct {
	Path           string
	FieldKind      string
	IsEnabled      bool
	IsRequiredByUs bool
	DisplayLabel   string
	DisplayOrder   int
	WebsiteSection string
	Notes          string
	SampleValue    string
}

type websiteSectionOptionView struct {
	Value string
	Label string
}

type templateDetailPageData struct {
	pageData
	Detail                *templatesvc.TemplateDetail
	Fields                []templateFieldView
	WebsiteSectionOptions []websiteSectionOptionView
}

type buildFieldView struct {
	Index            int
	AnchorID         string
	Path             string
	DisplayLabel     string
	SampleValue      string
	Value            string
	SuggestedValue   string
	SuggestionReason string
	SuggestionStatus string
	SuggestionSource string
}

type buildFieldGroupView struct {
	Title  string
	Fields []buildFieldView
}

type buildFieldSectionView struct {
	Key            string
	Title          string
	Description    string
	EditableGroups []buildFieldGroupView
	EditableFields []buildFieldView
	DisabledFields []buildFieldView
}

type semanticSlotPreviewView struct {
	Slot     string
	Count    int
	Examples string
}

type pendingField struct {
	Field domain.TemplateField
	View  buildFieldView
}

type fieldRole struct {
	Label string
	Order int
}

var blockIDPattern = regexp.MustCompile(`(?i)(?:^|[_.])([mcr]\d{3,})(?:[_.]|$)`)
var looseBlockIDPattern = regexp.MustCompile(`(?i)([mcr]\d{3,})`)

var knownBlockAreas = map[string]string{
	"m1710": "Hero / Haupttitel",
	"c7886": "Intro / Einleitung",
	"r4830": "Services",
	"m4178": "Gallery / Medien",
	"c2929": "Ueber uns / About",
	"r4748": "Team",
	"r1508": "Testimonials",
	"c1165": "CTA / Highlight / Banner",
}

type buildNewPageData struct {
	pageData
	Templates           []domain.Template
	Drafts              []domain.BuildDraft
	SelectedDraftID     string
	SelectedTemplateID  int64
	SelectedManifestID  string
	FieldSections       []buildFieldSectionView
	EditableFields      []buildFieldView
	EnabledFields       []buildFieldView
	SuggestionStateJSON string
	AutofillFocusID     string
	ShowDebug           bool
	Form                buildFormInput
	SemanticSlots       []semanticSlotPreviewView
}

type buildFormInput struct {
	DraftID            string
	DraftSource        string
	DraftStatus        string
	DraftNotes         string
	RequestName        string
	CompanyName        string
	BusinessType       string
	Username           string
	Email              string
	Phone              string
	OrgNumber          string
	StartDate          string
	Mission            string
	DescriptionShort   string
	DescriptionLong    string
	SiteLanguage       string
	AddressLine1       string
	AddressLine2       string
	AddressCity        string
	AddressRegion      string
	AddressZIP         string
	AddressCountry     string
	WebsiteURL         string
	WebsiteSummary     string
	LocaleStyle        string
	MarketStyle        string
	AddressMode        string
	ContentTone        string
	PromptInstructions string
	MasterPrompt       string
	PromptBlocks       []domain.PromptBlockConfig
}

type buildDetailPageData struct {
	pageData
	Build              *domain.SiteBuild
	EffectiveGlobal    []byte
	CanPoll            bool
	CanFetchEditorURL  bool
	AutoRefreshSeconds int
}

func NewUI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service, settings store.SettingsStore, suggestionGenerator mapping.SuggestionGenerator, cfg config.Config, render htmlRenderer, logger *slog.Logger) *UI {
	if logger == nil {
		logger = slog.Default()
	}
	return &UI{
		templateSvc:         templateSvc,
		onboardSvc:          onboardSvc,
		draftSvc:            draftSvc,
		buildSvc:            buildSvc,
		settings:            settings,
		suggestionGenerator: suggestionGenerator,
		cfg:                 cfg,
		render:              render,
		logger:              logger,
	}
}

func (u *UI) Home(w http.ResponseWriter, r *http.Request) {
	templates, err := u.templateSvc.ListTemplates(r.Context())
	if err != nil {
		u.render.Render(w, "home", homePageData{pageData: basePageData(r, "Home", "/"), TemplateCount: 0})
		return
	}
	u.render.Render(w, "home", homePageData{pageData: basePageData(r, "Home", "/"), TemplateCount: len(templates)})
}

func (u *UI) Settings(w http.ResponseWriter, r *http.Request) {
	settings := u.loadPromptSettings(r.Context())
	activeProvider := domain.NormalizeLLMProvider(settings.LLMActiveProvider)
	modelOptions := domain.LLMModelsByProvider(activeProvider)
	u.render.Render(w, "settings", settingsPageData{
		pageData:               basePageData(r, "Settings", "/settings"),
		QCBaseURL:              u.cfg.QCBaseURL,
		PollIntervalSeconds:    u.cfg.PollIntervalSeconds,
		PollTimeoutSeconds:     u.cfg.PollTimeoutSeconds,
		PollMaxConcurrent:      u.cfg.PollMaxConcurrent,
		TokenConfigured:        strings.TrimSpace(u.cfg.QCToken) != "",
		LanguageOutputMode:     "EN",
		LLMProviderOptions:     domain.LLMProviderOptions(),
		LLMModelOptions:        modelOptions,
		LLMActiveProvider:      activeProvider,
		LLMActiveModel:         domain.NormalizeLLMModel(activeProvider, settings.LLMActiveModel),
		LLMBaseURL:             strings.TrimSpace(settings.LLMBaseURL),
		LLMTemperature:         domain.NormalizeLLMTemperature(settings.LLMTemperature),
		LLMMaxTokens:           domain.NormalizeLLMMaxTokens(settings.LLMMaxTokens),
		OpenAIKeyConfigured:    strings.TrimSpace(settings.OpenAIAPIKeyEncrypted) != "",
		AnthropicKeyConfigured: strings.TrimSpace(settings.AnthropicAPIKeyEncrypted) != "",
		GoogleKeyConfigured:    strings.TrimSpace(settings.GoogleAPIKeyEncrypted) != "",
		XAIKeyConfigured:       strings.TrimSpace(settings.XAIAPIKeyEncrypted) != "",
		OllamaKeyConfigured:    strings.TrimSpace(settings.OllamaAPIKeyEncrypted) != "",
		MasterPrompt:           settings.MasterPrompt,
		PromptBlocks:           settings.PromptBlocks,
	})
}

func (u *UI) SavePromptSettings(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/settings?err=invalid+form", http.StatusSeeOther)
		return
	}
	settings := u.loadPromptSettings(r.Context())
	settings.MasterPrompt = domain.NormalizeMasterPrompt(r.FormValue("master_prompt"))
	settings.PromptBlocks = parsePromptBlocksFromRequest(r)
	if err := u.settings.UpsertSettings(r.Context(), settings); err != nil {
		http.Redirect(w, r, "/settings?err="+urlQuery(err.Error()), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, "/settings?msg=prompt+settings+saved", http.StatusSeeOther)
}

func (u *UI) SaveLLMSettings(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/settings?err=invalid+form", http.StatusSeeOther)
		return
	}
	settings, err := applyLLMSettingsForm(u.loadPromptSettings(r.Context()), r)
	if err != nil {
		http.Redirect(w, r, "/settings?err="+urlQuery(err.Error()), http.StatusSeeOther)
		return
	}
	if err := u.settings.UpsertSettings(r.Context(), settings); err != nil {
		http.Redirect(w, r, "/settings?err="+urlQuery(err.Error()), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, "/settings?msg=llm+settings+saved", http.StatusSeeOther)
}

func (u *UI) ValidateLLMSettings(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/settings?err=invalid+form", http.StatusSeeOther)
		return
	}
	settings, err := applyLLMSettingsForm(u.loadPromptSettings(r.Context()), r)
	if err != nil {
		http.Redirect(w, r, "/settings?err="+urlQuery(err.Error()), http.StatusSeeOther)
		return
	}
	if err := validateLLMProviderConfig(r.Context(), settings, u.logger); err != nil {
		http.Redirect(w, r, "/settings?err="+urlQuery(err.Error()), http.StatusSeeOther)
		return
	}
	msg := fmt.Sprintf("llm provider config validated (%s / %s)", settings.LLMActiveProvider, settings.LLMActiveModel)
	http.Redirect(w, r, "/settings?msg="+urlQuery(msg), http.StatusSeeOther)
}

func (u *UI) Templates(w http.ResponseWriter, r *http.Request) {
	templates, err := u.templateSvc.ListTemplates(r.Context())
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	u.render.Render(w, "templates", templatesPageData{pageData: basePageData(r, "Templates", "/templates"), Templates: templates})
}

func (u *UI) SyncTemplates(w http.ResponseWriter, r *http.Request) {
	if _, err := u.templateSvc.SyncAITemplates(r.Context()); err != nil {
		http.Redirect(w, r, "/templates?err="+urlQuery(err.Error()), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, "/templates?msg=sync+done", http.StatusSeeOther)
}

func (u *UI) TemplateDetail(w http.ResponseWriter, r *http.Request) {
	templateID, ok := parseTemplateID(w, r)
	if !ok {
		return
	}
	detail, err := u.templateSvc.GetTemplateDetail(r.Context(), templateID)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}
	fields := make([]templateFieldView, 0, len(detail.Fields))
	for _, f := range detail.Fields {
		fields = append(fields, templateFieldView{
			Path:           f.Path,
			FieldKind:      f.FieldKind,
			IsEnabled:      f.IsEnabled,
			IsRequiredByUs: f.IsRequiredByUs,
			DisplayLabel:   f.DisplayLabel,
			DisplayOrder:   f.DisplayOrder,
			WebsiteSection: domain.NormalizeWebsiteSection(f.WebsiteSection),
			Notes:          f.Notes,
			SampleValue:    f.SampleValue,
		})
	}
	u.render.Render(w, "template_detail", templateDetailPageData{
		pageData:              basePageData(r, "Template Detail", "/templates"),
		Detail:                detail,
		Fields:                fields,
		WebsiteSectionOptions: websiteSectionOptions(),
	})
}

func (u *UI) OnboardTemplate(w http.ResponseWriter, r *http.Request) {
	templateID, ok := parseTemplateID(w, r)
	if !ok {
		return
	}
	if _, _, err := u.onboardSvc.OnboardTemplate(r.Context(), templateID); err != nil {
		http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery(err.Error())), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, fmt.Sprintf("/templates/%d?msg=onboarding+done", templateID), http.StatusSeeOther)
}

func (u *UI) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) {
	templateID, ok := parseTemplateID(w, r)
	if !ok {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid form")), http.StatusSeeOther)
		return
	}

	count, _ := strconv.Atoi(r.FormValue("field_count"))
	patches := make([]onboarding.FieldPatch, 0, count)
	for i := 0; i < count; i++ {
		path := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_path_%d", i)))
		if path == "" {
			continue
		}
		enabled := r.FormValue(fmt.Sprintf("field_enabled_%d", i)) == "on"
		required := r.FormValue(fmt.Sprintf("field_required_%d", i)) == "on"
		label := r.FormValue(fmt.Sprintf("field_label_%d", i))
		notes := r.FormValue(fmt.Sprintf("field_notes_%d", i))
		websiteSection := r.FormValue(fmt.Sprintf("field_website_section_%d", i))
		order, err := strconv.Atoi(strings.TrimSpace(r.FormValue(fmt.Sprintf("field_order_%d", i))))
		if err != nil {
			http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery("invalid display order")), http.StatusSeeOther)
			return
		}
		patches = append(patches, onboarding.FieldPatch{
			Path:           path,
			IsEnabled:      boolPtr(enabled),
			IsRequiredByUs: boolPtr(required),
			DisplayLabel:   strPtr(label),
			DisplayOrder:   intPtr(order),
			WebsiteSection: strPtr(websiteSection),
			Notes:          strPtr(notes),
		})
	}

	manifestID := r.FormValue("manifest_id")
	if _, _, err := u.onboardSvc.UpdateTemplateFields(r.Context(), templateID, manifestID, patches); err != nil {
		http.Redirect(w, r, fmt.Sprintf("/templates/%d?err=%s", templateID, urlQuery(err.Error())), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, fmt.Sprintf("/templates/%d?msg=fields+saved", templateID), http.StatusSeeOther)
}

func (u *UI) BuildNew(w http.ResponseWriter, r *http.Request) {
	settings := u.loadPromptSettings(r.Context())
	selectedTemplateID, _ := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("template_id")), 10, 64)
	selectedDraftID := strings.TrimSpace(r.URL.Query().Get("draft_id"))
	form := buildFormInput{
		DraftID:      selectedDraftID,
		DraftSource:  "ui",
		DraftStatus:  "draft",
		MasterPrompt: settings.MasterPrompt,
		PromptBlocks: clonePromptBlocks(settings.PromptBlocks),
	}
	fieldValues := map[string]string{}
	suggestionState := domain.DraftSuggestionState{}
	if selectedDraftID != "" {
		draft, err := u.draftSvc.GetDraft(r.Context(), selectedDraftID)
		if err == nil {
			selectedTemplateID = draft.TemplateID
			form = buildFormInputFromDraft(draft)
			fieldValues = parseFieldValuesJSON(draft.FieldValuesJSON)
			suggestionState = parseSuggestionStateJSON(draft.SuggestionStateJSON)
			form.MasterPrompt = settings.MasterPrompt
			form.PromptBlocks = mergePromptBlocks(form.PromptBlocks, settings.PromptBlocks)
		}
	}
	form.PromptBlocks = applyPromptBlockActivationDefaults(form.PromptBlocks, form)
	data, err := u.loadBuildNewPageData(r, basePageData(r, "New Build", "/builds/new"), selectedDraftID, selectedTemplateID, form, fieldValues, suggestionState)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	u.render.Render(w, "build_new", data)
}

func (u *UI) CreateBuild(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/builds/new?err=invalid+form", http.StatusSeeOther)
		return
	}

	form := buildFormInputFromRequest(r)
	form = u.applyPromptConfigForBuildFlow(r.Context(), form)
	fieldValues := parseBuildFieldValues(r)
	suggestionState := parseSuggestionStateFromRequest(r)
	globalData := buildsvc.BuildGlobalData(buildsvc.GlobalDataInput{
		CompanyName:      form.CompanyName,
		BusinessType:     form.BusinessType,
		Username:         form.Username,
		Email:            form.Email,
		Phone:            form.Phone,
		OrgNumber:        form.OrgNumber,
		StartDate:        form.StartDate,
		Mission:          form.Mission,
		DescriptionShort: form.DescriptionShort,
		DescriptionLong:  form.DescriptionLong,
		SiteLanguage:     form.SiteLanguage,
		AddressLine1:     form.AddressLine1,
		AddressLine2:     form.AddressLine2,
		AddressCity:      form.AddressCity,
		AddressRegion:    form.AddressRegion,
		AddressZIP:       form.AddressZIP,
		AddressCountry:   form.AddressCountry,
	})

	templateID, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("template_id")), 10, 64)
	if err != nil || templateID <= 0 {
		http.Redirect(w, r, "/builds/new?err=invalid+template", http.StatusSeeOther)
		return
	}

	result, err := u.buildSvc.StartBuild(r.Context(), buildsvc.StartBuildRequest{
		TemplateID:  templateID,
		RequestName: form.RequestName,
		GlobalData:  globalData,
		FieldValues: fieldValues,
	})
	if err != nil {
		data, loadErr := u.loadBuildNewPageData(r, pageData{
			Title:   "New Build",
			Err:     err.Error(),
			Current: "/builds/new",
		}, form.DraftID, templateID, form, fieldValues, suggestionState)
		if loadErr != nil {
			http.Error(w, loadErr.Error(), http.StatusBadRequest)
			return
		}
		u.render.Render(w, "build_new", data)
		return
	}

	if form.DraftID != "" {
		_, _ = u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
			DraftID:         form.DraftID,
			TemplateID:      int64Ptr(templateID),
			ManifestID:      strings.TrimSpace(r.FormValue("manifest_id")),
			Source:          form.DraftSource,
			RequestName:     form.RequestName,
			GlobalData:      globalData,
			FieldValues:     fieldValues,
			DraftContext:    buildDraftContextFromForm(form, globalData),
			SuggestionState: &suggestionState,
			Status:          "submitted",
			Notes:           form.DraftNotes,
		})
	}

	http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=build+started", result.BuildID), http.StatusSeeOther)
}

func (u *UI) SaveDraft(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/builds/new?err=invalid+form", http.StatusSeeOther)
		return
	}
	form := buildFormInputFromRequest(r)
	form = u.applyPromptConfigForBuildFlow(r.Context(), form)
	fieldValues := parseBuildFieldValues(r)
	suggestionState := parseSuggestionStateFromRequest(r)
	templateID, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("template_id")), 10, 64)
	if err != nil || templateID <= 0 {
		http.Redirect(w, r, "/builds/new?err=invalid+template", http.StatusSeeOther)
		return
	}
	globalData := buildsvc.BuildGlobalData(buildsvc.GlobalDataInput{
		CompanyName:      form.CompanyName,
		BusinessType:     form.BusinessType,
		Username:         form.Username,
		Email:            form.Email,
		Phone:            form.Phone,
		OrgNumber:        form.OrgNumber,
		StartDate:        form.StartDate,
		Mission:          form.Mission,
		DescriptionShort: form.DescriptionShort,
		DescriptionLong:  form.DescriptionLong,
		SiteLanguage:     form.SiteLanguage,
		AddressLine1:     form.AddressLine1,
		AddressLine2:     form.AddressLine2,
		AddressCity:      form.AddressCity,
		AddressRegion:    form.AddressRegion,
		AddressZIP:       form.AddressZIP,
		AddressCountry:   form.AddressCountry,
	})
	draft, err := u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
		DraftID:         form.DraftID,
		TemplateID:      int64Ptr(templateID),
		ManifestID:      strings.TrimSpace(r.FormValue("manifest_id")),
		Source:          form.DraftSource,
		RequestName:     form.RequestName,
		GlobalData:      globalData,
		FieldValues:     fieldValues,
		DraftContext:    buildDraftContextFromForm(form, globalData),
		SuggestionState: &suggestionState,
		Status:          defaultDraftStatus(form.DraftStatus),
		Notes:           form.DraftNotes,
	})
	if err != nil {
		data, loadErr := u.loadBuildNewPageData(r, pageData{
			Title:   "New Build",
			Err:     err.Error(),
			Current: "/builds/new",
		}, form.DraftID, templateID, form, fieldValues, suggestionState)
		if loadErr != nil {
			http.Error(w, loadErr.Error(), http.StatusBadRequest)
			return
		}
		u.render.Render(w, "build_new", data)
		return
	}
	http.Redirect(w, r, fmt.Sprintf("/builds/new?template_id=%d&draft_id=%s&msg=draft+saved", templateID, urlQuery(draft.ID)), http.StatusSeeOther)
}

func (u *UI) AutofillDraft(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/builds/new?err=invalid+form", http.StatusSeeOther)
		return
	}
	form := buildFormInputFromRequest(r)
	form = u.applyPromptConfigForBuildFlow(r.Context(), form)
	fieldValues := parseBuildFieldValues(r)
	suggestionState := parseSuggestionStateFromRequest(r)
	templateID, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("template_id")), 10, 64)
	if err != nil || templateID <= 0 {
		http.Redirect(w, r, "/builds/new?err=invalid+template", http.StatusSeeOther)
		return
	}

	detail, err := u.templateSvc.GetTemplateDetail(r.Context(), templateID)
	if err != nil || detail.Manifest == nil {
		http.Redirect(w, r, "/builds/new?err=template+detail+missing", http.StatusSeeOther)
		return
	}

	globalData := buildGlobalDataFromForm(form)
	draftContext := buildDraftContextFromForm(form, globalData)
	action, targetFieldPath := parseAutofillAction(strings.TrimSpace(r.FormValue("autofill_action")))
	focusFieldPath := targetFieldPath
	now := time.Now().UTC()
	activeSettings := u.loadPromptSettings(r.Context())
	activeProvider := domain.NormalizeLLMProvider(activeSettings.LLMActiveProvider)
	activeModel := domain.NormalizeLLMModel(activeProvider, activeSettings.LLMActiveModel)
	autofillStart := time.Now()
	u.logger.InfoContext(r.Context(), "autofill action",
		"component", "autofill",
		"step", "action_start",
		"status", "start",
		"action", action,
		"provider", activeProvider,
		"model", activeModel,
		"draft_id", strings.TrimSpace(form.DraftID),
		"template_id", templateID,
	)
	req := mapping.SuggestionRequest{
		TemplateID:   templateID,
		DraftID:      strings.TrimSpace(form.DraftID),
		Fields:       detail.Fields,
		GlobalData:   globalData,
		DraftContext: draftContext,
		MasterPrompt: form.MasterPrompt,
		PromptBlocks: form.PromptBlocks,
		Existing:     fieldValues,
	}

	msg := "autofill ready"
	switch action {
	case "generate_all":
		suggestionState = mapping.GenerateAllSuggestions(r.Context(), u.suggestionGenerator, req, suggestionState, now)
		msg = "suggestions generated"
	case "regenerate_all":
		suggestionState = mapping.RegenerateAllSuggestions(r.Context(), u.suggestionGenerator, req, suggestionState, now)
		msg = "suggestions regenerated"
	case "apply_all":
		fieldValues, suggestionState = mapping.ApplyAllSuggestions(fieldValues, suggestionState, now)
		msg = "all suggestions applied"
	case "apply_all_empty":
		fieldValues, suggestionState = mapping.ApplySuggestionsToEmptyFields(fieldValues, suggestionState, now)
		msg = "suggestions applied to empty fields"
	case "apply_field":
		fieldValues, suggestionState = mapping.ApplySuggestionToField(fieldValues, suggestionState, targetFieldPath, now)
		msg = "field suggestion applied"
	case "regenerate_field":
		suggestionState = mapping.RegenerateFieldSuggestion(r.Context(), u.suggestionGenerator, req, suggestionState, targetFieldPath, now)
		msg = "field suggestion regenerated"
	default:
		msg = "unknown autofill action"
	}
	u.logger.InfoContext(r.Context(), "autofill state transition",
		"component", "autofill",
		"step", "pre_action_summary",
		"action", action,
		"state_count", len(suggestionState.ByFieldPath),
		"state_sources", summarizeSuggestionSources(suggestionState),
	)
	u.logger.DebugContext(r.Context(), "autofill state transition",
		"component", "autofill",
		"step", "pre_action_summary",
		"action", action,
		"sample_sources", sampleSuggestionSources(suggestionState, 5),
	)
	sourceCounts := summarizeSuggestionSources(suggestionState)
	u.logger.InfoContext(r.Context(), "autofill action",
		"component", "autofill",
		"step", "action_finish",
		"status", "success",
		"action", action,
		"provider", activeProvider,
		"model", activeModel,
		"draft_id", strings.TrimSpace(form.DraftID),
		"template_id", templateID,
		"suggestion_count", len(suggestionState.ByFieldPath),
		"sources", sourceCounts,
		"duration_ms", time.Since(autofillStart).Milliseconds(),
	)

	if strings.TrimSpace(form.DraftID) != "" {
		_, _ = u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
			DraftID:         form.DraftID,
			TemplateID:      int64Ptr(templateID),
			ManifestID:      strings.TrimSpace(r.FormValue("manifest_id")),
			Source:          form.DraftSource,
			RequestName:     form.RequestName,
			GlobalData:      globalData,
			FieldValues:     fieldValues,
			DraftContext:    draftContext,
			SuggestionState: &suggestionState,
			Status:          defaultDraftStatus(form.DraftStatus),
			Notes:           form.DraftNotes,
		})
	}

	data, loadErr := u.loadBuildNewPageData(r, pageData{
		Title:   "New Build",
		Msg:     msg,
		Current: "/builds/new",
	}, form.DraftID, templateID, form, fieldValues, suggestionState)
	if loadErr != nil {
		http.Error(w, loadErr.Error(), http.StatusBadRequest)
		return
	}
	data.AutofillFocusID = fieldAnchorID(focusFieldPath)
	u.render.Render(w, "build_new", data)
}

func (u *UI) BuildDetail(w http.ResponseWriter, r *http.Request) {
	buildID := strings.TrimSpace(chi.URLParam(r, "id"))
	build, err := u.buildSvc.GetBuild(r.Context(), buildID)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}

	status := strings.ToLower(strings.TrimSpace(build.QCStatus))
	canPoll := status == "queued" || status == "processing"
	canFetchEditor := (status == "done" || status == "failed" || status == "timeout") &&
		build.QCSiteID != nil &&
		strings.TrimSpace(build.QCEditorURL) == ""
	autoRefresh := 0
	if canPoll && u.cfg.PollIntervalSeconds > 0 {
		autoRefresh = u.cfg.PollIntervalSeconds
	}
	effectiveGlobal := build.GlobalDataJSON
	if payloadGlobal, err := extractGlobalDataFromFinalPayload(build.FinalSitesPayload); err == nil && len(payloadGlobal) > 0 {
		effectiveGlobal = payloadGlobal
	}
	u.render.Render(w, "build_detail", buildDetailPageData{
		pageData:           basePageData(r, "Build Detail", "/builds"),
		Build:              build,
		EffectiveGlobal:    effectiveGlobal,
		CanPoll:            canPoll,
		CanFetchEditorURL:  canFetchEditor,
		AutoRefreshSeconds: autoRefresh,
	})
}

func (u *UI) PollBuild(w http.ResponseWriter, r *http.Request) {
	buildID := strings.TrimSpace(chi.URLParam(r, "id"))
	if err := u.buildSvc.PollOnce(r.Context(), buildID); err != nil {
		http.Redirect(w, r, fmt.Sprintf("/builds/%s?err=%s", buildID, urlQuery(err.Error())), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=poll+done", buildID), http.StatusSeeOther)
}

func (u *UI) FetchEditorURL(w http.ResponseWriter, r *http.Request) {
	buildID := strings.TrimSpace(chi.URLParam(r, "id"))
	if err := u.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
		http.Redirect(w, r, fmt.Sprintf("/builds/%s?err=%s", buildID, urlQuery(err.Error())), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, fmt.Sprintf("/builds/%s?msg=editor+url+loaded", buildID), http.StatusSeeOther)
}

func basePageData(r *http.Request, title, current string) pageData {
	q := r.URL.Query()
	return pageData{Title: title, Msg: q.Get("msg"), Err: q.Get("err"), Current: current}
}

func parseTemplateID(w http.ResponseWriter, r *http.Request) (int64, bool) {
	rawID := chi.URLParam(r, "id")
	templateID, err := strconv.ParseInt(rawID, 10, 64)
	if err != nil {
		http.Error(w, "invalid template id", http.StatusBadRequest)
		return 0, false
	}
	return templateID, true
}

func urlQuery(s string) string {
	return url.QueryEscape(s)
}

func applyLLMSettingsForm(settings domain.AppSettings, r *http.Request) (domain.AppSettings, error) {
	next := settings
	next.LLMActiveProvider = domain.NormalizeLLMProvider(r.FormValue("llm_provider"))
	next.LLMActiveModel = domain.NormalizeLLMModel(next.LLMActiveProvider, r.FormValue("llm_model"))
	next.LLMBaseURL = strings.TrimSpace(r.FormValue("llm_base_url"))

	tempRaw := strings.TrimSpace(r.FormValue("llm_temperature"))
	if tempRaw == "" {
		next.LLMTemperature = domain.NormalizeLLMTemperature(next.LLMTemperature)
	} else {
		temp, err := strconv.ParseFloat(tempRaw, 64)
		if err != nil {
			return settings, fmt.Errorf("invalid llm temperature")
		}
		next.LLMTemperature = domain.NormalizeLLMTemperature(temp)
	}

	maxTokensRaw := strings.TrimSpace(r.FormValue("llm_max_tokens"))
	if maxTokensRaw == "" {
		next.LLMMaxTokens = domain.NormalizeLLMMaxTokens(next.LLMMaxTokens)
	} else {
		maxTokens, err := strconv.Atoi(maxTokensRaw)
		if err != nil {
			return settings, fmt.Errorf("invalid llm max tokens")
		}
		next.LLMMaxTokens = domain.NormalizeLLMMaxTokens(maxTokens)
	}

	if value := strings.TrimSpace(r.FormValue("llm_api_key_openai")); value != "" {
		next.OpenAIAPIKeyEncrypted = value
	}
	if value := strings.TrimSpace(r.FormValue("llm_api_key_anthropic")); value != "" {
		next.AnthropicAPIKeyEncrypted = value
	}
	if value := strings.TrimSpace(r.FormValue("llm_api_key_google")); value != "" {
		next.GoogleAPIKeyEncrypted = value
	}
	if value := strings.TrimSpace(r.FormValue("llm_api_key_xai")); value != "" {
		next.XAIAPIKeyEncrypted = value
	}
	if value := strings.TrimSpace(r.FormValue("llm_api_key_ollama")); value != "" {
		next.OllamaAPIKeyEncrypted = value
	}
	return next, nil
}

func validateLLMProviderConfig(ctx context.Context, settings domain.AppSettings, logger *slog.Logger) error {
	if logger == nil {
		logger = slog.Default()
	}
	started := time.Now()
	provider := domain.NormalizeLLMProvider(settings.LLMActiveProvider)
	model := domain.NormalizeLLMModel(provider, settings.LLMActiveModel)
	baseURL := strings.TrimSpace(settings.LLMBaseURL)
	logger.InfoContext(ctx, "validate llm provider config",
		"component", "autofill",
		"step", "validate_provider",
		"status", "start",
		"provider", provider,
		"model", model,
		"base_url", baseURL,
	)
	if strings.TrimSpace(model) == "" {
		logger.WarnContext(ctx, "validate llm provider config",
			"component", "autofill",
			"step", "validate_provider",
			"status", "failed",
			"provider", provider,
			"model", model,
			"base_url", baseURL,
			"error", "no active model configured",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return fmt.Errorf("no active model configured")
	}
	if baseURL != "" {
		parsed, err := url.Parse(baseURL)
		if err != nil || strings.TrimSpace(parsed.Scheme) == "" || strings.TrimSpace(parsed.Host) == "" {
			logger.WarnContext(ctx, "validate llm provider config",
				"component", "autofill",
				"step", "validate_provider",
				"status", "failed",
				"provider", provider,
				"model", model,
				"base_url", baseURL,
				"error", "invalid llm base url",
				"duration_ms", time.Since(started).Milliseconds(),
			)
			return fmt.Errorf("invalid llm base url")
		}
	}
	apiKey := domain.LLMAPIKeyForProvider(provider, settings)
	if provider != domain.LLMProviderOllama && strings.TrimSpace(apiKey) == "" {
		logger.WarnContext(ctx, "validate llm provider config",
			"component", "autofill",
			"step", "validate_provider",
			"status", "failed",
			"provider", provider,
			"model", model,
			"base_url", baseURL,
			"error", "missing api key",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return fmt.Errorf("api key for provider %s is not configured", provider)
	}

	runtimeFactory := llmruntime.NewFactory(10 * time.Second)
	client, err := runtimeFactory.ClientFor(provider)
	if err != nil {
		logger.WarnContext(ctx, "validate llm provider config",
			"component", "autofill",
			"step", "validate_provider",
			"status", "failed",
			"provider", provider,
			"model", model,
			"base_url", baseURL,
			"error", shortError(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return err
	}

	temperature := domain.NormalizeLLMTemperature(settings.LLMTemperature)
	maxTokens := domain.NormalizeLLMMaxTokens(settings.LLMMaxTokens)
	validationTokens := maxTokens
	if validationTokens > 64 {
		validationTokens = 64
	}
	if validationTokens < 16 {
		validationTokens = 16
	}
	resp, err := client.Generate(ctx, llmruntime.Request{
		Provider:     provider,
		Model:        model,
		BaseURL:      baseURL,
		APIKey:       apiKey,
		Temperature:  &temperature,
		MaxTokens:    &validationTokens,
		SystemPrompt: "You validate LLM connectivity for settings. Answer with plain text OK.",
		UserPrompt:   "Return OK",
	})
	if err != nil {
		logger.WarnContext(ctx, "validate llm provider config",
			"component", "autofill",
			"step", "validate_provider",
			"status", "failed",
			"provider", provider,
			"model", model,
			"base_url", baseURL,
			"error", shortError(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return fmt.Errorf("provider validation failed (%s/%s): %w", provider, model, err)
	}
	if strings.TrimSpace(resp) == "" {
		logger.WarnContext(ctx, "validate llm provider config",
			"component", "autofill",
			"step", "validate_provider",
			"status", "failed",
			"provider", provider,
			"model", model,
			"base_url", baseURL,
			"error", "empty response",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return fmt.Errorf("provider validation failed (%s/%s): empty response", provider, model)
	}
	logger.InfoContext(ctx, "validate llm provider config",
		"component", "autofill",
		"step", "validate_provider",
		"status", "success",
		"provider", provider,
		"model", model,
		"base_url", baseURL,
		"duration_ms", time.Since(started).Milliseconds(),
	)
	logger.DebugContext(ctx, "validate llm provider config",
		"component", "autofill",
		"step", "validate_provider",
		"status", "success",
		"provider", provider,
		"model", model,
		"base_url", baseURL,
		"response_snippet", trimSnippet(resp, 40),
	)
	return nil
}

func boolPtr(v bool) *bool    { return &v }
func intPtr(v int) *int       { return &v }
func int64Ptr(v int64) *int64 { return &v }
func strPtr(v string) *string {
	return &v
}

func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedDraftID string, selectedTemplateID int64, form buildFormInput, fieldValues map[string]string, suggestionState domain.DraftSuggestionState) (buildNewPageData, error) {
	if strings.TrimSpace(form.MasterPrompt) == "" {
		form.MasterPrompt = domain.SeedMasterPrompt
	}
	form.PromptBlocks = domain.NormalizePromptBlocks(form.PromptBlocks)
	form.PromptBlocks = applyPromptBlockActivationDefaults(form.PromptBlocks, form)

	templates, err := u.templateSvc.ListTemplates(r.Context())
	if err != nil {
		return buildNewPageData{}, err
	}
	drafts, err := u.draftSvc.ListDrafts(r.Context(), 50)
	if err != nil {
		return buildNewPageData{}, err
	}

	data := buildNewPageData{
		pageData:            page,
		Templates:           templates,
		Drafts:              drafts,
		SelectedDraftID:     selectedDraftID,
		SelectedTemplateID:  selectedTemplateID,
		SuggestionStateJSON: encodeSuggestionStateJSON(suggestionState),
		ShowDebug:           parseDebugMode(r),
		Form:                form,
	}
	if selectedTemplateID <= 0 {
		return data, nil
	}

	detail, err := u.templateSvc.GetTemplateDetail(r.Context(), selectedTemplateID)
	if err != nil || detail.Manifest == nil {
		return data, nil
	}
	data.SelectedManifestID = detail.Manifest.ID
	data.EditableFields, data.FieldSections = buildFieldSections(detail.Fields, fieldValues, suggestionState.ByFieldPath)
	data.EnabledFields = data.EditableFields
	data.SemanticSlots = semanticSlotPreview(mapping.MapTemplateFieldsToSemanticSlots(detail.Fields))
	return data, nil
}

func (u *UI) applyPromptConfigForBuildFlow(ctx context.Context, form buildFormInput) buildFormInput {
	settings := u.loadPromptSettings(ctx)
	form.MasterPrompt = settings.MasterPrompt
	form.PromptBlocks = clonePromptBlocks(settings.PromptBlocks)
	if strings.TrimSpace(form.DraftID) == "" {
		return form
	}

	draft, err := u.draftSvc.GetDraft(ctx, strings.TrimSpace(form.DraftID))
	if err != nil || draft == nil {
		return form
	}
	mergeDraftContextIntoForm(&form, draft.DraftContextJSON)
	form.MasterPrompt = settings.MasterPrompt
	form.PromptBlocks = mergePromptBlocks(form.PromptBlocks, settings.PromptBlocks)
	form.PromptBlocks = applyPromptBlockActivationDefaults(form.PromptBlocks, form)
	return form
}

func buildFieldSections(fields []domain.TemplateField, fieldValues map[string]string, suggestions map[string]domain.DraftSuggestion) ([]buildFieldView, []buildFieldSectionView) {
	sectionOrder := []string{
		domain.WebsiteSectionHero,
		domain.WebsiteSectionIntro,
		domain.WebsiteSectionServices,
		domain.WebsiteSectionAbout,
		domain.WebsiteSectionTeam,
		domain.WebsiteSectionTestimonials,
		domain.WebsiteSectionCTA,
		domain.WebsiteSectionContact,
		domain.WebsiteSectionGallery,
		domain.WebsiteSectionFooter,
		domain.WebsiteSectionOther,
	}

	sectionDescriptions := map[string]string{
		domain.WebsiteSectionHero:         "Headline-nahe Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionIntro:        "Intro-/Einleitungs-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionServices:     "Services-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionAbout:        "About-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionTeam:         "Team-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionTestimonials: "Testimonial-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionCTA:          "CTA-/Highlight-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionContact:      "Kontakt-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionGallery:      "Media/Gallery-Felder, Bildfelder bleiben im MVP nicht editierbar.",
		domain.WebsiteSectionFooter:       "Footer-Felder, bevorzugt nach Block-ID gruppiert.",
		domain.WebsiteSectionOther:        "Aktive Textfelder ausserhalb der Kern-Sections, bevorzugt nach Block-ID gruppiert.",
	}

	sectionsByKey := make(map[string]buildFieldSectionView, len(sectionOrder))
	pendingByKey := make(map[string][]pendingField, len(sectionOrder))
	for _, key := range sectionOrder {
		sectionsByKey[key] = buildFieldSectionView{
			Key:         key,
			Title:       domain.WebsiteSectionLabel(key),
			Description: sectionDescriptions[key],
		}
	}

	for _, f := range fields {
		targetSection := preferredBuildSection(f)
		if isMediaOrGalleryField(f) || targetSection == domain.WebsiteSectionGallery {
			labelFallback := domain.WebsiteSectionLabel(domain.WebsiteSectionGallery) + " - " + humanizeKey(f.KeyName)
			if blockID := extractBlockID(f); blockID != "" {
				labelFallback = "Media - " + blockGroupTitle(blockID)
			}
			media := sectionsByKey[domain.WebsiteSectionGallery]
			media.DisabledFields = append(media.DisabledFields, buildFieldView{
				AnchorID:     fieldAnchorID(f.Path),
				Path:         f.Path,
				DisplayLabel: effectiveLabel(f, labelFallback),
				SampleValue:  f.SampleValue,
				Value:        "",
			})
			sectionsByKey[domain.WebsiteSectionGallery] = media
			continue
		}
		if !f.IsEnabled || !strings.EqualFold(strings.TrimSpace(f.FieldKind), "text") {
			continue
		}
		suggestion := suggestions[f.Path]
		pf := pendingField{
			Field: f,
			View: buildFieldView{
				AnchorID:         fieldAnchorID(f.Path),
				Path:             f.Path,
				DisplayLabel:     effectiveLabel(f, humanizeKey(f.KeyName)),
				SampleValue:      f.SampleValue,
				Value:            strings.TrimSpace(fieldValues[f.Path]),
				SuggestedValue:   strings.TrimSpace(suggestion.Value),
				SuggestionReason: strings.TrimSpace(suggestion.Reason),
				SuggestionStatus: strings.TrimSpace(suggestion.Status),
				SuggestionSource: strings.TrimSpace(suggestion.Source),
			},
		}
		pendingByKey[targetSection] = append(pendingByKey[targetSection], pf)
	}

	for _, key := range sectionOrder {
		section := sectionsByKey[key]
		items := pendingByKey[key]
		switch key {
		case domain.WebsiteSectionServices:
			section = applyServicesGrouping(section, items)
		case domain.WebsiteSectionTestimonials:
			section = applyTestimonialsGrouping(section, items)
		case domain.WebsiteSectionHero, domain.WebsiteSectionIntro, domain.WebsiteSectionAbout, domain.WebsiteSectionTeam, domain.WebsiteSectionCTA, domain.WebsiteSectionContact, domain.WebsiteSectionFooter:
			section = applyTextGrouping(section, items)
		case domain.WebsiteSectionOther:
			section = applyOtherGrouping(section, items)
		case domain.WebsiteSectionGallery:
			// Gallery fields are handled as disabled entries only in this MVP.
		default:
			section = applyOtherGrouping(section, items)
		}
		sectionsByKey[key] = section
	}

	sections := make([]buildFieldSectionView, 0, len(sectionOrder))
	for _, key := range sectionOrder {
		sections = append(sections, sectionsByKey[key])
	}
	return assignEditableIndexes(sections)
}

func applyServicesGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	return applyBlockFirstGrouping(section, fields, "Services", applyServicesGroupingFallback)
}

func applyServicesGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	titles := make([]pendingField, 0)
	descriptions := make([]pendingField, 0)
	other := make([]buildFieldView, 0)

	for _, pf := range fields {
		key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName))
		switch {
		case strings.HasPrefix(key, "servicestitle_") || strings.HasPrefix(key, "servicestitle"):
			titles = append(titles, pf)
		case strings.HasPrefix(key, "servicesdescription_") || strings.HasPrefix(key, "servicesdescription"):
			descriptions = append(descriptions, pf)
		default:
			pf.View.DisplayLabel = effectiveLabel(pf.Field, "Services - "+humanizeKey(pf.Field.KeyName))
			other = append(other, pf.View)
		}
	}

	maxCount := len(titles)
	if len(descriptions) > maxCount {
		maxCount = len(descriptions)
	}
	for i := 0; i < maxCount; i++ {
		block := buildFieldGroupView{Title: fmt.Sprintf("Service %d", i+1)}
		if i < len(titles) {
			item := titles[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Service %d - Titel", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if i < len(descriptions) {
			item := descriptions[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Service %d - Beschreibung", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if len(block.Fields) > 0 {
			section.EditableGroups = append(section.EditableGroups, block)
		}
	}
	section.EditableFields = append(section.EditableFields, other...)
	return section
}

func applyTestimonialsGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	return applyBlockFirstGrouping(section, fields, "Testimonials", applyTestimonialsGroupingFallback)
}

func applyTestimonialsGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	names := make([]pendingField, 0)
	titles := make([]pendingField, 0)
	descriptions := make([]pendingField, 0)
	other := make([]buildFieldView, 0)

	for _, pf := range fields {
		key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName))
		switch {
		case strings.HasPrefix(key, "testimonialsname_") || strings.HasPrefix(key, "testimonialsname"):
			names = append(names, pf)
		case strings.HasPrefix(key, "testimonialstitle_") || strings.HasPrefix(key, "testimonialstitle"):
			titles = append(titles, pf)
		case strings.HasPrefix(key, "testimonialsdescription_") || strings.HasPrefix(key, "testimonialsdescription"):
			descriptions = append(descriptions, pf)
		default:
			pf.View.DisplayLabel = effectiveLabel(pf.Field, "Testimonials - "+humanizeKey(pf.Field.KeyName))
			other = append(other, pf.View)
		}
	}

	maxCount := len(names)
	if len(titles) > maxCount {
		maxCount = len(titles)
	}
	if len(descriptions) > maxCount {
		maxCount = len(descriptions)
	}
	for i := 0; i < maxCount; i++ {
		block := buildFieldGroupView{Title: fmt.Sprintf("Testimonial %d", i+1)}
		if i < len(names) {
			item := names[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Name", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if i < len(titles) {
			item := titles[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Titel", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if i < len(descriptions) {
			item := descriptions[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Testimonial %d - Beschreibung", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if len(block.Fields) > 0 {
			section.EditableGroups = append(section.EditableGroups, block)
		}
	}
	section.EditableFields = append(section.EditableFields, other...)
	return section
}

func applyTextGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	return applyBlockFirstGrouping(section, fields, "Text", applyTextGroupingFallback)
}

func applyTextGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	titles := make([]pendingField, 0)
	descriptions := make([]pendingField, 0)
	names := make([]pendingField, 0)
	other := make([]buildFieldView, 0)

	for _, pf := range fields {
		key := strings.ToLower(strings.TrimSpace(pf.Field.KeyName))
		switch {
		case strings.HasPrefix(key, "texttitle_") || strings.HasPrefix(key, "texttitle") || strings.HasPrefix(key, "exttitle_") || strings.HasPrefix(key, "exttitle"):
			titles = append(titles, pf)
		case strings.HasPrefix(key, "textdescription_") || strings.HasPrefix(key, "textdescription") || strings.HasPrefix(key, "extdescription_") || strings.HasPrefix(key, "extdescription"):
			descriptions = append(descriptions, pf)
		case strings.HasPrefix(key, "textname_") || strings.HasPrefix(key, "textname") || strings.HasPrefix(key, "extname_") || strings.HasPrefix(key, "extname"):
			names = append(names, pf)
		default:
			pf.View.DisplayLabel = effectiveLabel(pf.Field, "Text - "+humanizeKey(pf.Field.KeyName))
			other = append(other, pf.View)
		}
	}

	maxCount := len(titles)
	if len(descriptions) > maxCount {
		maxCount = len(descriptions)
	}
	if len(names) > maxCount {
		maxCount = len(names)
	}
	for i := 0; i < maxCount; i++ {
		block := buildFieldGroupView{Title: fmt.Sprintf("Textblock %d", i+1)}
		if i < len(titles) {
			item := titles[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Titel", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if i < len(descriptions) {
			item := descriptions[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Beschreibung", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if i < len(names) {
			item := names[i]
			item.View.DisplayLabel = effectiveLabel(item.Field, fmt.Sprintf("Textblock %d - Name", i+1))
			block.Fields = append(block.Fields, item.View)
		}
		if len(block.Fields) > 0 {
			section.EditableGroups = append(section.EditableGroups, block)
		}
	}
	section.EditableFields = append(section.EditableFields, other...)
	return section
}

func applyOtherGrouping(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	return applyBlockFirstGrouping(section, fields, "", applyOtherGroupingFallback)
}

func applyOtherGroupingFallback(section buildFieldSectionView, fields []pendingField) buildFieldSectionView {
	for _, pf := range fields {
		pf.View.DisplayLabel = effectiveLabel(pf.Field, normalizedSectionTitle(pf.Field.Section)+" - "+humanizeKey(pf.Field.KeyName))
		section.EditableFields = append(section.EditableFields, pf.View)
	}
	sort.SliceStable(section.EditableFields, func(i, j int) bool {
		return section.EditableFields[i].Path < section.EditableFields[j].Path
	})
	return section
}

func applyBlockFirstGrouping(section buildFieldSectionView, fields []pendingField, fallbackPrefix string, fallback func(buildFieldSectionView, []pendingField) buildFieldSectionView) buildFieldSectionView {
	grouped := map[string][]pendingField{}
	withoutBlockID := make([]pendingField, 0)
	for _, pf := range fields {
		blockID := extractBlockID(pf.Field)
		if blockID == "" {
			withoutBlockID = append(withoutBlockID, pf)
			continue
		}
		grouped[blockID] = append(grouped[blockID], pf)
	}

	if len(grouped) > 0 {
		blockIDs := make([]string, 0, len(grouped))
		for blockID := range grouped {
			blockIDs = append(blockIDs, blockID)
		}
		sort.SliceStable(blockIDs, func(i, j int) bool {
			li, lj := blockSortRank(blockIDs[i]), blockSortRank(blockIDs[j])
			if li != lj {
				return li < lj
			}
			return blockIDs[i] < blockIDs[j]
		})

		for _, blockID := range blockIDs {
			items := grouped[blockID]
			sort.SliceStable(items, func(i, j int) bool {
				ri := deriveFieldRole(items[i].Field.KeyName)
				rj := deriveFieldRole(items[j].Field.KeyName)
				if ri.Order != rj.Order {
					return ri.Order < rj.Order
				}
				return items[i].Field.Path < items[j].Field.Path
			})

			group := buildFieldGroupView{Title: blockGroupTitle(blockID)}
			for _, item := range items {
				role := deriveFieldRole(item.Field.KeyName)
				fallbackLabel := role.Label
				if fallbackLabel == "" {
					fallbackLabel = humanizeKey(item.Field.KeyName)
				}
				item.View.DisplayLabel = effectiveLabel(item.Field, fallbackLabel)
				group.Fields = append(group.Fields, item.View)
			}
			if len(group.Fields) > 0 {
				section.EditableGroups = append(section.EditableGroups, group)
			}
		}
	}

	if len(withoutBlockID) > 0 {
		if fallback != nil {
			return fallback(section, withoutBlockID)
		}
		for _, pf := range withoutBlockID {
			labelPrefix := fallbackPrefix
			if labelPrefix == "" {
				labelPrefix = normalizedSectionTitle(pf.Field.Section)
			}
			pf.View.DisplayLabel = effectiveLabel(pf.Field, labelPrefix+" - "+humanizeKey(pf.Field.KeyName))
			section.EditableFields = append(section.EditableFields, pf.View)
		}
	}
	return section
}

func extractBlockID(f domain.TemplateField) string {
	candidates := []string{f.KeyName, f.Path}
	for _, candidate := range candidates {
		normalized := strings.ToLower(strings.TrimSpace(candidate))
		if normalized == "" {
			continue
		}
		if match := blockIDPattern.FindStringSubmatch(normalized); len(match) > 1 {
			return match[1]
		}
		if match := looseBlockIDPattern.FindStringSubmatch(normalized); len(match) > 1 {
			return match[1]
		}
	}
	return ""
}

func blockGroupTitle(blockID string) string {
	blockID = strings.ToLower(strings.TrimSpace(blockID))
	if blockID == "" {
		return "Unbekannter Block"
	}
	if known, ok := knownBlockAreas[blockID]; ok {
		return fmt.Sprintf("%s (%s)", known, blockID)
	}
	return "Block " + blockID
}

func blockSortRank(blockID string) int {
	switch strings.ToLower(strings.TrimSpace(blockID)) {
	case "m1710":
		return 10
	case "c7886":
		return 20
	case "r4830":
		return 30
	case "c2929":
		return 40
	case "r4748":
		return 50
	case "r1508":
		return 60
	case "c1165":
		return 70
	case "m4178":
		return 80
	default:
		return 1000
	}
}

func deriveFieldRole(key string) fieldRole {
	normalized := strings.ToLower(strings.TrimSpace(key))
	switch {
	case strings.Contains(normalized, "subtitle"):
		return fieldRole{Label: "Untertitel", Order: 15}
	case strings.Contains(normalized, "title"):
		return fieldRole{Label: "Titel", Order: 20}
	case strings.Contains(normalized, "description"):
		return fieldRole{Label: "Beschreibung", Order: 30}
	case strings.Contains(normalized, "name"):
		return fieldRole{Label: "Name", Order: 40}
	case strings.Contains(normalized, "button") || strings.Contains(normalized, "cta"):
		return fieldRole{Label: "CTA Text", Order: 50}
	default:
		return fieldRole{Label: humanizeKey(key), Order: 100}
	}
}

func assignEditableIndexes(sections []buildFieldSectionView) ([]buildFieldView, []buildFieldSectionView) {
	editable := make([]buildFieldView, 0)
	nextIndex := 0
	for si := range sections {
		for gi := range sections[si].EditableGroups {
			for fi := range sections[si].EditableGroups[gi].Fields {
				sections[si].EditableGroups[gi].Fields[fi].Index = nextIndex
				editable = append(editable, sections[si].EditableGroups[gi].Fields[fi])
				nextIndex++
			}
		}
		for fi := range sections[si].EditableFields {
			sections[si].EditableFields[fi].Index = nextIndex
			editable = append(editable, sections[si].EditableFields[fi])
			nextIndex++
		}
	}
	return editable, sections
}

func preferredBuildSection(f domain.TemplateField) string {
	websiteSection := strings.TrimSpace(f.WebsiteSection)
	if websiteSection != "" {
		normalized := domain.NormalizeWebsiteSection(websiteSection)
		if normalized == domain.WebsiteSectionServiceItem {
			return domain.WebsiteSectionServices
		}
		return normalized
	}
	return fallbackBuildSection(f)
}

func fallbackBuildSection(f domain.TemplateField) string {
	switch normalizedSection(f.Section) {
	case "services":
		return domain.WebsiteSectionServices
	case "testimonials":
		return domain.WebsiteSectionTestimonials
	case "text":
		if isMediaOrGalleryField(f) {
			return domain.WebsiteSectionGallery
		}
		return domain.WebsiteSectionOther
	default:
		if isMediaOrGalleryField(f) {
			return domain.WebsiteSectionGallery
		}
		return domain.WebsiteSectionOther
	}
}

func normalizedSection(raw string) string {
	section := strings.ToLower(strings.TrimSpace(raw))
	switch section {
	case "ext":
		return "text"
	default:
		return section
	}
}

func normalizedSectionTitle(raw string) string {
	switch normalizedSection(raw) {
	case "text":
		return "Text"
	case "services":
		return "Services"
	case "testimonials":
		return "Testimonials"
	case "gallery", "media":
		return "Media"
	default:
		return "Feld"
	}
}

func isMediaOrGalleryField(f domain.TemplateField) bool {
	if strings.EqualFold(strings.TrimSpace(f.FieldKind), "image") {
		return true
	}
	section := strings.ToLower(strings.TrimSpace(f.Section))
	key := strings.ToLower(strings.TrimSpace(f.KeyName))
	path := strings.ToLower(strings.TrimSpace(f.Path))
	if section == "gallery" || section == "media" {
		return true
	}
	hints := []string{"gallery", "image", "img", "photo", "picture"}
	for _, hint := range hints {
		if strings.Contains(section, hint) || strings.Contains(key, hint) || strings.Contains(path, hint) {
			return true
		}
	}
	return false
}

func effectiveLabel(f domain.TemplateField, fallback string) string {
	if !isRawPathLikeLabel(f.DisplayLabel, f.Path) {
		return strings.TrimSpace(f.DisplayLabel)
	}
	return strings.TrimSpace(fallback)
}

func isRawPathLikeLabel(label string, path string) bool {
	l := strings.TrimSpace(label)
	if l == "" {
		return true
	}
	if strings.EqualFold(l, strings.TrimSpace(path)) {
		return true
	}
	if strings.Contains(l, ".") || strings.Contains(l, "_") {
		return true
	}
	return false
}

func humanizeKey(key string) string {
	raw := strings.TrimSpace(key)
	if raw == "" {
		return "Inhalt"
	}
	base := raw
	if idx := strings.Index(base, "_"); idx > 0 {
		base = base[:idx]
	}
	runes := make([]rune, 0, len(base)+4)
	for i, r := range base {
		if i > 0 && unicode.IsUpper(r) {
			runes = append(runes, ' ')
		}
		runes = append(runes, r)
	}
	human := strings.TrimSpace(string(runes))
	if human == "" {
		return "Inhalt"
	}
	words := strings.Fields(strings.ToLower(human))
	for i := range words {
		if len(words[i]) > 0 {
			words[i] = strings.ToUpper(words[i][:1]) + words[i][1:]
		}
	}
	return strings.Join(words, " ")
}

func buildFormInputFromRequest(r *http.Request) buildFormInput {
	form := buildFormInput{
		DraftID:          strings.TrimSpace(r.FormValue("draft_id")),
		DraftSource:      strings.TrimSpace(r.FormValue("draft_source")),
		DraftStatus:      strings.TrimSpace(r.FormValue("draft_status")),
		DraftNotes:       strings.TrimSpace(r.FormValue("draft_notes")),
		RequestName:      strings.TrimSpace(r.FormValue("request_name")),
		CompanyName:      strings.TrimSpace(r.FormValue("company_name")),
		BusinessType:     strings.TrimSpace(r.FormValue("business_type")),
		Username:         strings.TrimSpace(r.FormValue("username")),
		Email:            strings.TrimSpace(r.FormValue("email")),
		Phone:            strings.TrimSpace(r.FormValue("phone")),
		OrgNumber:        strings.TrimSpace(r.FormValue("org_number")),
		StartDate:        strings.TrimSpace(r.FormValue("start_date")),
		Mission:          strings.TrimSpace(r.FormValue("mission")),
		DescriptionShort: strings.TrimSpace(r.FormValue("description_short")),
		DescriptionLong:  strings.TrimSpace(r.FormValue("description_long")),
		SiteLanguage:     strings.TrimSpace(r.FormValue("site_language")),
		AddressLine1:     strings.TrimSpace(r.FormValue("address_line1")),
		AddressLine2:     strings.TrimSpace(r.FormValue("address_line2")),
		AddressCity:      strings.TrimSpace(r.FormValue("address_city")),
		AddressRegion:    strings.TrimSpace(r.FormValue("address_region")),
		AddressZIP:       strings.TrimSpace(r.FormValue("address_zip")),
		AddressCountry:   strings.TrimSpace(r.FormValue("address_country")),
		WebsiteURL:       strings.TrimSpace(r.FormValue("website_url")),
		WebsiteSummary:   strings.TrimSpace(r.FormValue("website_summary")),
		LocaleStyle:      strings.TrimSpace(r.FormValue("locale_style")),
		MarketStyle:      strings.TrimSpace(r.FormValue("market_style")),
		AddressMode:      strings.TrimSpace(r.FormValue("address_mode")),
		ContentTone:      strings.TrimSpace(r.FormValue("content_tone")),
	}
	return form
}

func buildFormInputFromDraft(draft *domain.BuildDraft) buildFormInput {
	form := buildFormInput{
		DraftID:     draft.ID,
		DraftSource: draft.Source,
		DraftStatus: draft.Status,
		DraftNotes:  draft.Notes,
		RequestName: draft.RequestName,
	}
	mergeGlobalDataIntoForm(&form, draft.GlobalDataJSON)
	mergeDraftContextIntoForm(&form, draft.DraftContextJSON)
	return form
}

func parseBuildFieldValues(r *http.Request) map[string]string {
	fieldValues := map[string]string{}
	count, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("field_count")))
	for i := 0; i < count; i++ {
		path := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_path_%d", i)))
		value := strings.TrimSpace(r.FormValue(fmt.Sprintf("field_value_%d", i)))
		if path != "" {
			fieldValues[path] = value
		}
	}
	return fieldValues
}

func parseSuggestionStateFromRequest(r *http.Request) domain.DraftSuggestionState {
	return parseSuggestionStateRaw(strings.TrimSpace(r.FormValue("suggestion_state_json")))
}

func parseSuggestionStateJSON(raw []byte) domain.DraftSuggestionState {
	return parseSuggestionStateRaw(strings.TrimSpace(string(raw)))
}

func parseSuggestionStateRaw(raw string) domain.DraftSuggestionState {
	if strings.TrimSpace(raw) == "" {
		return domain.DraftSuggestionState{ByFieldPath: map[string]domain.DraftSuggestion{}}
	}
	var state domain.DraftSuggestionState
	if err := json.Unmarshal([]byte(raw), &state); err != nil {
		return domain.DraftSuggestionState{ByFieldPath: map[string]domain.DraftSuggestion{}}
	}
	if state.ByFieldPath == nil {
		state.ByFieldPath = map[string]domain.DraftSuggestion{}
	}
	return state
}

func encodeSuggestionStateJSON(state domain.DraftSuggestionState) string {
	normalized := state
	if normalized.ByFieldPath == nil {
		normalized.ByFieldPath = map[string]domain.DraftSuggestion{}
	}
	raw, err := json.Marshal(normalized)
	if err != nil {
		return `{"byFieldPath":{}}`
	}
	return string(raw)
}

func parseAutofillAction(raw string) (string, string) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return "", ""
	}
	parts := strings.SplitN(value, "::", 2)
	action := strings.TrimSpace(parts[0])
	if len(parts) == 1 {
		return action, ""
	}
	return action, strings.TrimSpace(parts[1])
}

func parseDebugMode(r *http.Request) bool {
	if r == nil {
		return false
	}
	value := strings.ToLower(strings.TrimSpace(r.FormValue("debug")))
	switch value {
	case "1", "true", "on", "yes":
		return true
	default:
		return false
	}
}

func summarizeSuggestionSources(state domain.DraftSuggestionState) map[string]int {
	if len(state.ByFieldPath) == 0 {
		return map[string]int{}
	}
	out := map[string]int{}
	for _, suggestion := range state.ByFieldPath {
		source := strings.TrimSpace(suggestion.Source)
		if source == "" {
			source = "unknown"
		}
		out[source]++
	}
	return out
}

func sampleSuggestionSources(state domain.DraftSuggestionState, limit int) map[string]string {
	if limit <= 0 || len(state.ByFieldPath) == 0 {
		return map[string]string{}
	}
	paths := make([]string, 0, len(state.ByFieldPath))
	for path := range state.ByFieldPath {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	if len(paths) > limit {
		paths = paths[:limit]
	}
	out := make(map[string]string, len(paths))
	for _, path := range paths {
		source := strings.TrimSpace(state.ByFieldPath[path].Source)
		if source == "" {
			source = "unknown"
		}
		out[path] = source
	}
	return out
}

func shortError(err error) string {
	if err == nil {
		return ""
	}
	message := strings.TrimSpace(err.Error())
	if len(message) > 180 {
		return message[:180] + "..."
	}
	return message
}

func trimSnippet(value string, max int) string {
	trimmed := strings.TrimSpace(value)
	if max <= 0 || len(trimmed) <= max {
		return trimmed
	}
	return trimmed[:max] + "..."
}

func fieldAnchorID(fieldPath string) string {
	path := strings.TrimSpace(strings.ToLower(fieldPath))
	if path == "" {
		return ""
	}
	var b strings.Builder
	b.Grow(len(path) + len("field-"))
	b.WriteString("field-")
	lastDash := false
	for _, r := range path {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
			b.WriteRune(r)
			lastDash = false
			continue
		}
		if !lastDash {
			b.WriteByte('-')
			lastDash = true
		}
	}
	out := strings.Trim(b.String(), "-")
	if out == "" || out == "field" {
		return "field-anchor"
	}
	return out
}

func extractGlobalDataFromFinalPayload(raw []byte) ([]byte, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	var payload struct {
		GlobalData map[string]any `json:"globalData"`
	}
	if err := json.Unmarshal(raw, &payload); err != nil {
		return nil, err
	}
	if len(payload.GlobalData) == 0 {
		return nil, nil
	}
	data, err := json.Marshal(payload.GlobalData)
	if err != nil {
		return nil, err
	}
	return data, nil
}

func parseFieldValuesJSON(raw []byte) map[string]string {
	out := map[string]string{}
	if len(raw) == 0 {
		return out
	}
	_ = json.Unmarshal(raw, &out)
	return out
}

func mergeGlobalDataIntoForm(form *buildFormInput, raw []byte) {
	if form == nil || len(raw) == 0 {
		return
	}
	var global map[string]any
	if err := json.Unmarshal(raw, &global); err != nil {
		return
	}
	form.CompanyName = getString(global["companyName"])
	form.BusinessType = getString(global["businessType"])
	form.Username = getString(global["username"])
	form.Email = getString(global["email"])
	form.Phone = getString(global["phone"])
	form.OrgNumber = getString(global["orgNumber"])
	form.StartDate = getString(global["startDate"])
	form.Mission = getString(global["mission"])
	form.DescriptionShort = getString(global["descriptionShort"])
	form.DescriptionLong = getString(global["descriptionLong"])
	form.SiteLanguage = getString(global["siteLanguage"])
	address, _ := global["address"].(map[string]any)
	form.AddressLine1 = getString(address["line1"])
	form.AddressLine2 = getString(address["line2"])
	form.AddressCity = getString(address["city"])
	form.AddressRegion = getString(address["region"])
	form.AddressZIP = getString(address["zip"])
	form.AddressCountry = getString(address["country"])
}

func mergeDraftContextIntoForm(form *buildFormInput, raw []byte) {
	if form == nil || len(raw) == 0 {
		return
	}
	var ctx domain.DraftContext
	if err := json.Unmarshal(raw, &ctx); err != nil {
		return
	}
	if strings.TrimSpace(form.BusinessType) == "" {
		form.BusinessType = strings.TrimSpace(ctx.LLM.BusinessType)
	}
	form.WebsiteURL = strings.TrimSpace(ctx.LLM.WebsiteURL)
	form.WebsiteSummary = strings.TrimSpace(ctx.LLM.WebsiteSummary)
	form.LocaleStyle = strings.TrimSpace(ctx.LLM.StyleProfile.LocaleStyle)
	form.MarketStyle = strings.TrimSpace(ctx.LLM.StyleProfile.MarketStyle)
	form.AddressMode = strings.TrimSpace(ctx.LLM.StyleProfile.AddressMode)
	form.ContentTone = strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone)
	form.PromptInstructions = strings.TrimSpace(ctx.LLM.StyleProfile.PromptInstructions)
	form.PromptBlocks = clonePromptBlocks(ctx.LLM.Prompt.Blocks)
}

func buildGlobalDataFromForm(form buildFormInput) map[string]any {
	return buildsvc.BuildGlobalData(buildsvc.GlobalDataInput{
		CompanyName:      form.CompanyName,
		BusinessType:     form.BusinessType,
		Username:         form.Username,
		Email:            form.Email,
		Phone:            form.Phone,
		OrgNumber:        form.OrgNumber,
		StartDate:        form.StartDate,
		Mission:          form.Mission,
		DescriptionShort: form.DescriptionShort,
		DescriptionLong:  form.DescriptionLong,
		SiteLanguage:     form.SiteLanguage,
		AddressLine1:     form.AddressLine1,
		AddressLine2:     form.AddressLine2,
		AddressCity:      form.AddressCity,
		AddressRegion:    form.AddressRegion,
		AddressZIP:       form.AddressZIP,
		AddressCountry:   form.AddressCountry,
	})
}

func buildDraftContextFromForm(form buildFormInput, globalData map[string]any) *domain.DraftContext {
	businessType := strings.TrimSpace(form.BusinessType)
	if businessType == "" {
		businessType = strings.TrimSpace(getString(globalData["businessType"]))
	}
	return &domain.DraftContext{
		IntakeSource: strings.TrimSpace(form.DraftSource),
		LLM: domain.DraftLLMContext{
			BusinessType:   businessType,
			WebsiteURL:     strings.TrimSpace(form.WebsiteURL),
			WebsiteSummary: strings.TrimSpace(form.WebsiteSummary),
			StyleProfile: domain.DraftStyleProfile{
				LocaleStyle:        strings.TrimSpace(form.LocaleStyle),
				MarketStyle:        strings.TrimSpace(form.MarketStyle),
				AddressMode:        strings.TrimSpace(form.AddressMode),
				ContentTone:        strings.TrimSpace(form.ContentTone),
				PromptInstructions: strings.TrimSpace(form.PromptInstructions),
			},
			Prompt: domain.DraftPromptConfig{
				Blocks: clonePromptBlocks(form.PromptBlocks),
			},
		},
	}
}

func (u *UI) loadPromptSettings(ctx context.Context) domain.AppSettings {
	defaultProvider := domain.DefaultLLMProvider()
	settings := domain.AppSettings{
		QCBaseURL:              u.cfg.QCBaseURL,
		QCBearerTokenEncrypted: u.cfg.QCToken,
		LanguageOutputMode:     "EN",
		JobPollIntervalSeconds: u.cfg.PollIntervalSeconds,
		JobPollTimeoutSeconds:  u.cfg.PollTimeoutSeconds,
		LLMActiveProvider:      defaultProvider,
		LLMActiveModel:         domain.NormalizeLLMModel(defaultProvider, ""),
		LLMTemperature:         domain.DefaultLLMTemperature(),
		LLMMaxTokens:           domain.DefaultLLMMaxTokens(),
		MasterPrompt:           domain.SeedMasterPrompt,
		PromptBlocks:           domain.DefaultPromptBlocks(),
	}
	if u.settings == nil {
		return settings
	}
	stored, err := u.settings.GetSettings(ctx)
	if err != nil || stored == nil {
		return settings
	}
	if strings.TrimSpace(stored.QCBaseURL) != "" {
		settings.QCBaseURL = strings.TrimSpace(stored.QCBaseURL)
	}
	if strings.TrimSpace(stored.QCBearerTokenEncrypted) != "" {
		settings.QCBearerTokenEncrypted = strings.TrimSpace(stored.QCBearerTokenEncrypted)
	}
	if strings.TrimSpace(stored.LanguageOutputMode) != "" {
		settings.LanguageOutputMode = strings.TrimSpace(stored.LanguageOutputMode)
	}
	if stored.JobPollIntervalSeconds > 0 {
		settings.JobPollIntervalSeconds = stored.JobPollIntervalSeconds
	}
	if stored.JobPollTimeoutSeconds > 0 {
		settings.JobPollTimeoutSeconds = stored.JobPollTimeoutSeconds
	}
	settings.LLMActiveProvider = domain.NormalizeLLMProvider(stored.LLMActiveProvider)
	settings.LLMActiveModel = domain.NormalizeLLMModel(settings.LLMActiveProvider, stored.LLMActiveModel)
	settings.LLMBaseURL = strings.TrimSpace(stored.LLMBaseURL)
	settings.LLMTemperature = domain.NormalizeLLMTemperature(stored.LLMTemperature)
	settings.LLMMaxTokens = domain.NormalizeLLMMaxTokens(stored.LLMMaxTokens)
	settings.OpenAIAPIKeyEncrypted = strings.TrimSpace(stored.OpenAIAPIKeyEncrypted)
	settings.AnthropicAPIKeyEncrypted = strings.TrimSpace(stored.AnthropicAPIKeyEncrypted)
	settings.GoogleAPIKeyEncrypted = strings.TrimSpace(stored.GoogleAPIKeyEncrypted)
	settings.XAIAPIKeyEncrypted = strings.TrimSpace(stored.XAIAPIKeyEncrypted)
	settings.OllamaAPIKeyEncrypted = strings.TrimSpace(stored.OllamaAPIKeyEncrypted)
	settings.MasterPrompt = domain.NormalizeMasterPrompt(stored.MasterPrompt)
	settings.PromptBlocks = domain.NormalizePromptBlocks(stored.PromptBlocks)
	return settings
}

func parsePromptBlocksFromRequest(r *http.Request) []domain.PromptBlockConfig {
	count, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("prompt_block_count")))
	if count <= 0 {
		return domain.DefaultPromptBlocks()
	}
	out := make([]domain.PromptBlockConfig, 0, count)
	for i := 0; i < count; i++ {
		id := strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_id_%d", i)))
		if id == "" {
			continue
		}
		out = append(out, domain.PromptBlockConfig{
			ID:          id,
			Label:       strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_label_%d", i))),
			Instruction: strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_instruction_%d", i))),
			Enabled:     strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_enabled_%d", i))) == "on",
		})
	}
	return domain.NormalizePromptBlocks(out)
}

func clonePromptBlocks(blocks []domain.PromptBlockConfig) []domain.PromptBlockConfig {
	if len(blocks) == 0 {
		return nil
	}
	out := make([]domain.PromptBlockConfig, len(blocks))
	copy(out, blocks)
	return out
}

func mergePromptBlocks(current []domain.PromptBlockConfig, defaults []domain.PromptBlockConfig) []domain.PromptBlockConfig {
	merged := make([]domain.PromptBlockConfig, 0, len(defaults))
	merged = append(merged, clonePromptBlocks(defaults)...)
	overrides := make(map[string]domain.PromptBlockConfig, len(current))
	for _, block := range current {
		id := strings.TrimSpace(block.ID)
		if id == "" {
			continue
		}
		overrides[id] = block
	}
	for i := range merged {
		if override, ok := overrides[merged[i].ID]; ok {
			if strings.TrimSpace(override.Label) != "" {
				merged[i].Label = strings.TrimSpace(override.Label)
			}
			if strings.TrimSpace(override.Instruction) != "" {
				merged[i].Instruction = strings.TrimSpace(override.Instruction)
			}
			merged[i].Enabled = override.Enabled
			delete(overrides, merged[i].ID)
		}
	}
	for _, override := range overrides {
		merged = append(merged, override)
	}
	return domain.NormalizePromptBlocks(merged)
}

func applyPromptBlockActivationDefaults(blocks []domain.PromptBlockConfig, form buildFormInput) []domain.PromptBlockConfig {
	out := clonePromptBlocks(blocks)
	if len(out) == 0 {
		return out
	}
	for i := range out {
		switch out[i].ID {
		case "business_type":
			if strings.TrimSpace(form.BusinessType) != "" {
				out[i].Enabled = true
			}
		case "website_summary":
			if strings.TrimSpace(form.WebsiteSummary) != "" {
				out[i].Enabled = true
			}
		case "address_mode":
			if strings.TrimSpace(form.AddressMode) != "" {
				out[i].Enabled = true
			}
		case "content_tone":
			if strings.TrimSpace(form.ContentTone) != "" {
				out[i].Enabled = true
			}
		case "free_instructions":
			if strings.TrimSpace(form.PromptInstructions) != "" {
				out[i].Enabled = true
			}
		}
	}
	return out
}

func semanticSlotPreview(mappingResult mapping.SemanticSlotMapping) []semanticSlotPreviewView {
	if len(mappingResult.BySlot) == 0 {
		return nil
	}
	slotKeys := make([]string, 0, len(mappingResult.BySlot))
	for slot := range mappingResult.BySlot {
		slotKeys = append(slotKeys, slot)
	}
	sort.Strings(slotKeys)

	out := make([]semanticSlotPreviewView, 0, len(slotKeys))
	for _, slot := range slotKeys {
		targets := mappingResult.BySlot[slot]
		examples := make([]string, 0, 2)
		for i := 0; i < len(targets) && i < 2; i++ {
			examples = append(examples, targets[i].FieldPath)
		}
		out = append(out, semanticSlotPreviewView{
			Slot:     slot,
			Count:    len(targets),
			Examples: strings.Join(examples, ", "),
		})
	}
	return out
}

func getString(v any) string {
	s, _ := v.(string)
	return strings.TrimSpace(s)
}

func defaultDraftStatus(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "reviewed", "submitted":
		return strings.ToLower(strings.TrimSpace(status))
	default:
		return "draft"
	}
}

func websiteSectionOptions() []websiteSectionOptionView {
	values := domain.WebsiteSectionOptions()
	out := make([]websiteSectionOptionView, 0, len(values))
	for _, value := range values {
		out = append(out, websiteSectionOptionView{
			Value: value,
			Label: domain.WebsiteSectionLabel(value),
		})
	}
	return out
}
