package handler import ( "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "floorvisualizer/internal/middleware" "floorvisualizer/internal/model" "floorvisualizer/internal/repository" ) func handleMe(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := middleware.UserID(r.Context()) switch r.Method { case http.MethodGet: user, err := repository.GetUserByID(db, userID) if err != nil { writeJSON(w, 404, map[string]string{"error": "user not found"}) return } writeJSON(w, 200, user) case http.MethodPut: var input struct { Name string `json:"name"` AvatarURL string `json:"avatar_url"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, 400, map[string]string{"error": "invalid request body"}) return } user, err := repository.UpdateUser(db, userID, input.Name, input.AvatarURL) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, user) default: writeErr(w, 405, "Method not allowed") } } } func handleFavorites(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := middleware.UserID(r.Context()) switch r.Method { case http.MethodGet: favs, err := repository.ListFavorites(db, userID) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if favs == nil { favs = []model.Favorite{} } writeJSON(w, 200, map[string]interface{}{"favorites": favs, "total": len(favs)}) case http.MethodPost: sku := r.URL.Query().Get("sku") if sku == "" || strings.Contains(sku, "/") { writeJSON(w, 400, map[string]string{"error": "sku is required"}) return } product, err := repository.GetProductBySKU(db, sku) if err != nil { writeJSON(w, 404, map[string]string{"error": "product not found"}) return } fav := model.Favorite{ SKU: product.SKU, Brand: product.Brand, SeriesName: product.SeriesName, StyleName: product.StyleName, MainImageURL: product.MainImageURL, Category: product.Category, Material: product.Material, ColorTone: product.ColorTone, PricePerSqft: product.PricePerSqft, } if err := repository.AddFavorite(db, userID, fav); err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 201, fav) case http.MethodDelete: sku := strings.TrimSpace(strings.TrimPrefix(r.URL.Path, "/user/favorites/")) if sku == "" || strings.Contains(sku, "/") { writeJSON(w, 400, map[string]string{"error": "missing sku"}) return } if err := repository.RemoveFavorite(db, userID, sku); err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, map[string]string{"ok": "removed"}) default: writeErr(w, 405, "Method not allowed") } } } func handleProjects(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := middleware.UserID(r.Context()) projectID := strings.TrimPrefix(r.URL.Path, "/user/projects/") if projectID != "" && projectID != r.URL.Path { handleSingleProject(w, r, db, userID, projectID) return } switch r.Method { case http.MethodGet: projects, err := repository.ListProjects(db, userID) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if projects == nil { projects = []model.Project{} } writeJSON(w, 200, map[string]interface{}{"projects": projects, "total": len(projects)}) case http.MethodPost: var input struct { Name string `json:"name"` OriginalImageURL string `json:"original_image_url"` GeneratedImageURL string `json:"generated_image_url"` FloorStyle string `json:"floor_style"` Pattern string `json:"pattern"` RoomType string `json:"room_type"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, 400, map[string]string{"error": "invalid request body"}) return } if input.Name == "" { input.Name = "Untitled" } project, err := repository.CreateProject(db, userID, input.Name, input.OriginalImageURL, input.GeneratedImageURL, input.FloorStyle, input.Pattern, input.RoomType) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 201, project) default: writeErr(w, 405, "Method not allowed") } } } func handleSingleProject(w http.ResponseWriter, r *http.Request, db *sql.DB, userID, projectID string) { switch r.Method { case http.MethodGet: project, err := repository.GetProject(db, projectID, userID) if err != nil { writeJSON(w, 404, map[string]string{"error": "project not found"}) return } writeJSON(w, 200, project) case http.MethodPut: var input struct{ Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, 400, map[string]string{"error": "invalid request body"}) return } project, err := repository.UpdateProject(db, projectID, userID, input.Name) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, project) case http.MethodDelete: if err := repository.DeleteProject(db, projectID, userID); err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, map[string]string{"ok": "deleted"}) default: writeErr(w, 405, "Method not allowed") } } func handleUploadAvatar(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := middleware.UserID(r.Context()) if err := r.ParseMultipartForm(5 << 20); err != nil { writeJSON(w, 400, map[string]string{"error": "image too large (max 5MB)"}) return } file, _, err := r.FormFile("image") if err != nil { writeJSON(w, 400, map[string]string{"error": "missing image file"}) return } defer file.Close() data, err := io.ReadAll(file) if err != nil || len(data) == 0 { writeJSON(w, 400, map[string]string{"error": "failed to read image"}) return } ct := http.DetectContentType(data) if !strings.HasPrefix(ct, "image/") { writeJSON(w, 400, map[string]string{"error": "file is not an image"}) return } hash := sha256.Sum256(data) name := hex.EncodeToString(hash[:])[:16] + "_" + fmt.Sprintf("%d", time.Now().UnixNano()) dir := "uploads/avatars" os.MkdirAll(dir, 0755) filePath := filepath.Join(dir, name+".jpg") if err := os.WriteFile(filePath, data, 0644); err != nil { writeJSON(w, 500, map[string]string{"error": "failed to save image"}) return } current, err := repository.GetUserByID(db, userID) if err != nil || current == nil { writeJSON(w, 500, map[string]string{"error": "user not found"}) return } avatarURL := "/uploads/avatars/" + name + ".jpg" user, err := repository.UpdateUser(db, userID, current.Name, avatarURL) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, user) } } // ── Brand view tracking ─────────────────────────────────── func handleBrandView(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeErr(w, 405, "Method not allowed") return } var body struct { Brand string `json:"brand"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeJSON(w, 400, map[string]string{"error": "invalid request body"}) return } if body.Brand == "" { writeJSON(w, 400, map[string]string{"error": "brand is required"}) return } userID := middleware.UserID(r.Context()) if userID == "" { writeJSON(w, 401, map[string]string{"error": "login required"}) return } if err := repository.RecordBrandView(db, userID, body.Brand); err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, map[string]string{"ok": "recorded"}) } } func handleFrequentBrands(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := middleware.UserID(r.Context()) if userID == "" { writeJSON(w, 401, map[string]string{"error": "login required"}) return } brands, err := repository.GetFrequentBrands(db, userID) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if brands == nil { brands = []repository.FrequentBrand{} } writeJSON(w, 200, map[string]interface{}{ "brands": brands, "total": len(brands), }) } }