package handlers import ( "encoding/json" "net/http" "strconv" "strings" "github.com/go-chi/chi/v5" "qctextbuilder/internal/buildsvc" "qctextbuilder/internal/domain" "qctextbuilder/internal/draftsvc" "qctextbuilder/internal/onboarding" "qctextbuilder/internal/templatesvc" ) type API struct { templateSvc *templatesvc.Service onboardSvc *onboarding.Service draftSvc *draftsvc.Service buildSvc buildsvc.Service } func NewAPI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service) *API { return &API{ templateSvc: templateSvc, onboardSvc: onboardSvc, draftSvc: draftSvc, buildSvc: buildSvc, } } func (a *API) Health(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"status": "ok"}) } func (a *API) SyncTemplates(w http.ResponseWriter, r *http.Request) { templates, err := a.templateSvc.SyncAITemplates(r.Context()) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates}) } func (a *API) ListTemplates(w http.ResponseWriter, r *http.Request) { templates, err := a.templateSvc.ListTemplates(r.Context()) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates}) } func (a *API) GetTemplateDetail(w http.ResponseWriter, r *http.Request) { rawID := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(rawID, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"}) return } detail, err := a.templateSvc.GetTemplateDetail(r.Context(), templateID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, detail) } func (a *API) OnboardTemplate(w http.ResponseWriter, r *http.Request) { rawID := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(rawID, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"}) return } manifest, fields, err := a.onboardSvc.OnboardTemplate(r.Context(), templateID) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "manifestId": manifest.ID, "fieldCount": len(fields), "status": "reviewed", }) } type updateTemplateFieldsRequest struct { ManifestID string `json:"manifestId"` Fields []updateTemplateFieldItem `json:"fields"` } type updateTemplateFieldItem struct { Path string `json:"path"` IsEnabled *bool `json:"isEnabled,omitempty"` IsRequiredByUs *bool `json:"isRequiredByUs,omitempty"` DisplayLabel *string `json:"displayLabel,omitempty"` DisplayOrder *int `json:"displayOrder,omitempty"` WebsiteSection *string `json:"websiteSection,omitempty"` Notes *string `json:"notes,omitempty"` } func (a *API) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) { rawID := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(rawID, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"}) return } var req updateTemplateFieldsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"}) return } if len(req.Fields) == 0 { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "fields is required"}) return } patches := make([]onboarding.FieldPatch, 0, len(req.Fields)) for _, f := range req.Fields { patches = append(patches, onboarding.FieldPatch{ Path: f.Path, IsEnabled: f.IsEnabled, IsRequiredByUs: f.IsRequiredByUs, DisplayLabel: f.DisplayLabel, DisplayOrder: f.DisplayOrder, WebsiteSection: f.WebsiteSection, Notes: f.Notes, }) } manifest, fields, err := a.onboardSvc.UpdateTemplateFields(r.Context(), templateID, req.ManifestID, patches) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "templateId": templateID, "manifestId": manifest.ID, "fieldCount": len(fields), "fields": fields, }) } func (a *API) StartBuild(w http.ResponseWriter, r *http.Request) { var req buildsvc.StartBuildRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"}) return } result, err := a.buildSvc.StartBuild(r.Context(), req) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusAccepted, result) } type upsertDraftRequest struct { TemplateID *int64 `json:"templateId,omitempty"` ManifestID string `json:"manifestId"` Source string `json:"source"` RequestName string `json:"requestName"` GlobalData map[string]any `json:"globalData"` FieldValues map[string]string `json:"fieldValues"` DraftContext *domain.DraftContext `json:"draftContext,omitempty"` SuggestionState *domain.DraftSuggestionState `json:"suggestionState,omitempty"` Status string `json:"status"` Notes string `json:"notes"` } type intakeDraftRequest struct { DraftID string `json:"draftId,omitempty"` Source string `json:"source"` RequestName string `json:"requestName"` TemplateID *int64 `json:"templateId,omitempty"` GlobalData map[string]any `json:"globalData"` Notes string `json:"notes"` WebsiteURL string `json:"websiteUrl,omitempty"` WebsiteSummary string `json:"websiteSummary,omitempty"` BusinessType string `json:"businessType,omitempty"` LocaleStyle string `json:"localeStyle,omitempty"` MarketStyle string `json:"marketStyle,omitempty"` AddressMode string `json:"addressMode,omitempty"` ContentTone string `json:"contentTone,omitempty"` PromptInstructions string `json:"promptInstructions,omitempty"` StyleProfile *domain.DraftStyleProfile `json:"styleProfile,omitempty"` } func (a *API) IntakeDraft(w http.ResponseWriter, r *http.Request) { var req intakeDraftRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"}) return } globalData := req.GlobalData if globalData == nil { globalData = map[string]any{} } if strings.TrimSpace(req.BusinessType) != "" && strings.TrimSpace(getMapString(globalData, "businessType")) == "" { globalData["businessType"] = strings.TrimSpace(req.BusinessType) } styleProfile := domain.DraftStyleProfile{ LocaleStyle: strings.TrimSpace(req.LocaleStyle), MarketStyle: strings.TrimSpace(req.MarketStyle), AddressMode: strings.TrimSpace(req.AddressMode), ContentTone: strings.TrimSpace(req.ContentTone), PromptInstructions: strings.TrimSpace(req.PromptInstructions), } if req.StyleProfile != nil { styleProfile = *req.StyleProfile if styleProfile.LocaleStyle == "" { styleProfile.LocaleStyle = strings.TrimSpace(req.LocaleStyle) } if styleProfile.MarketStyle == "" { styleProfile.MarketStyle = strings.TrimSpace(req.MarketStyle) } if styleProfile.AddressMode == "" { styleProfile.AddressMode = strings.TrimSpace(req.AddressMode) } if styleProfile.ContentTone == "" { styleProfile.ContentTone = strings.TrimSpace(req.ContentTone) } if styleProfile.PromptInstructions == "" { styleProfile.PromptInstructions = strings.TrimSpace(req.PromptInstructions) } } businessType := strings.TrimSpace(req.BusinessType) if businessType == "" { businessType = strings.TrimSpace(getMapString(globalData, "businessType")) } draftContext := &domain.DraftContext{ IntakeSource: strings.TrimSpace(req.Source), LLM: domain.DraftLLMContext{ BusinessType: businessType, WebsiteURL: strings.TrimSpace(req.WebsiteURL), WebsiteSummary: strings.TrimSpace(req.WebsiteSummary), StyleProfile: styleProfile, }, } draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ DraftID: strings.TrimSpace(req.DraftID), TemplateID: req.TemplateID, Source: defaultStr(req.Source, "intake-api"), RequestName: req.RequestName, GlobalData: globalData, FieldValues: map[string]string{}, DraftContext: draftContext, Status: "draft", Notes: req.Notes, }) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } if strings.TrimSpace(req.DraftID) == "" { writeJSON(w, http.StatusCreated, draft) return } writeJSON(w, http.StatusOK, draft) } func (a *API) ListDrafts(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit"))) drafts, err := a.draftSvc.ListDrafts(r.Context(), limit) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"count": len(drafts), "drafts": drafts}) } func (a *API) GetDraft(w http.ResponseWriter, r *http.Request) { draftID := strings.TrimSpace(chi.URLParam(r, "id")) draft, err := a.draftSvc.GetDraft(r.Context(), draftID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, draft) } func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) { draftID := strings.TrimSpace(chi.URLParam(r, "id")) var req upsertDraftRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"}) return } draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ DraftID: draftID, TemplateID: req.TemplateID, ManifestID: req.ManifestID, Source: req.Source, RequestName: req.RequestName, GlobalData: req.GlobalData, FieldValues: req.FieldValues, DraftContext: req.DraftContext, SuggestionState: req.SuggestionState, Status: req.Status, Notes: req.Notes, }) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, draft) } func (a *API) GetBuild(w http.ResponseWriter, r *http.Request) { buildID := chi.URLParam(r, "id") build, err := a.buildSvc.GetBuild(r.Context(), buildID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, build) } func (a *API) PollBuildOnce(w http.ResponseWriter, r *http.Request) { buildID := chi.URLParam(r, "id") if err := a.buildSvc.PollOnce(r.Context(), buildID); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } build, err := a.buildSvc.GetBuild(r.Context(), buildID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, build) } func (a *API) FetchBuildEditorURL(w http.ResponseWriter, r *http.Request) { buildID := chi.URLParam(r, "id") if err := a.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } build, err := a.buildSvc.GetBuild(r.Context(), buildID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()}) return } writeJSON(w, http.StatusOK, build) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func defaultStr(v, fallback string) string { if strings.TrimSpace(v) == "" { return fallback } return strings.TrimSpace(v) } func getMapString(values map[string]any, key string) string { if values == nil { return "" } raw, _ := values[key].(string) return raw }