package main import ( "database/sql" "fmt" "net/http" "strconv" "strings" "time" "github.com/labstack/echo/v4" ) func (a *App) handleScoreAdvice(c echo.Context) error { stage, err := validateStage(c.Param("stage")) if err != nil { return writeError(c, http.StatusBadRequest, err.Error()) } playerID, err := strconv.Atoi(c.Param("id")) if err != nil { return writeError(c, http.StatusBadRequest, "invalid player id") } if strings.TrimSpace(a.cfg.GeminiAPIKey) == "" { return writeError(c, http.StatusServiceUnavailable, "gemini api key is not configured") } proofImageData, err := a.loadScoreProofImage(stage, playerID) if err != nil { if err == sql.ErrNoRows { return writeError(c, http.StatusBadRequest, "no proof image found for this score") } return writeError(c, http.StatusInternalServerError, fmt.Sprintf("load proof image: %v", err)) } if strings.TrimSpace(proofImageData) == "" { return writeError(c, http.StatusBadRequest, "no proof image found for this score") } currentScore, _ := a.loadCurrentScore(stage, playerID) advice, err := a.generateScoreAdvice(stage, playerID, proofImageData, currentScore) if err != nil { return writeError(c, http.StatusBadGateway, err.Error()) } return c.JSON(http.StatusOK, advice) } func (a *App) loadScoreProofImage(stage string, playerID int) (string, error) { var imageData string err := a.db.QueryRow(`SELECT image_data FROM score_attachments WHERE stage = ? AND player_id = ?`, stage, playerID).Scan(&imageData) if err != nil { return "", err } return imageData, nil } func (a *App) loadCurrentScore(stage string, playerID int) (int, error) { var score int err := a.db.QueryRow(`SELECT score FROM scores WHERE stage = ? AND player_id = ?`, stage, playerID).Scan(&score) if err != nil { if err == sql.ErrNoRows { return 0, nil } return 0, err } return score, nil } func (a *App) buildAdviceResponse(stage string, playerID int, raw scoreAdviceModelResponse) ScoreAdviceResponse { advised := clampInt(raw.AdvisedScore, 0, 9999) reason := strings.TrimSpace(raw.Reason) if reason == "" { reason = "AI estimated the score from visible impacts." } return ScoreAdviceResponse{ Stage: stage, PlayerID: playerID, AdvisedScore: advised, Reason: reason, GeneratedAt: time.Now().UTC().Format(time.RFC3339), } } func clampInt(value, min, max int) int { if value < min { return min } if value > max { return max } return value }