package briefing import ( "fmt" "strings" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" ) type Weekend struct { Days []OutlookDay `json:"days"` Planning WeekendPlanning `json:"planning"` RelevantAlerts []forecast.AlertOverlap `json:"relevantAlerts,omitempty"` Discussion DiscussionContext `json:"discussion,omitempty"` WeatherStory *WeatherStoryContext `json:"weatherStory,omitempty"` } type WeekendPlanning struct { BestOutdoorWindows []OutdoorWindow `json:"bestOutdoorWindows,omitempty"` WorstWeatherWindows []OutdoorWindow `json:"worstWeatherWindows,omitempty"` RainStormTiming []string `json:"rainStormTiming,omitempty"` ComfortConcerns []string `json:"comfortConcerns,omitempty"` UncertaintyInputs []string `json:"uncertaintyInputs,omitempty"` } func BuildWeekend(ctx BuildContext, summaries []forecast.DailySummary) (Package, error) { if ctx.Resolved.Definition.ID != report.Weekend { return Package{}, fmt.Errorf("weekend briefing requires a weekend report definition") } if len(summaries) == 0 { return Package{}, fmt.Errorf("weekend forecast summaries are required") } pkg := Package{ Metadata: BuildMetadata(ctx), Weekend: &Weekend{ Discussion: buildDiscussion(summaries[0].Discussion), WeatherStory: buildWeatherStory(ctx.Bundle), }, } for _, summary := range summaries { pkg.Weekend.Days = append(pkg.Weekend.Days, buildOutlookDay(summary)) } pkg.Weekend.RelevantAlerts = collectOutlookAlerts(pkg.Weekend.Days) pkg.Weekend.Planning = buildWeekendPlanning(pkg.Weekend.Days, pkg.Weekend.Discussion, ctx.Bundle) return pkg, nil } func buildWeekendPlanning(days []OutlookDay, discussion DiscussionContext, bundle *forecast.Bundle) WeekendPlanning { planning := WeekendPlanning{} for _, day := range days { if day.OutdoorWindows.Best != nil { window := *day.OutdoorWindows.Best window.Daypart = day.Date + " " + window.Daypart planning.BestOutdoorWindows = append(planning.BestOutdoorWindows, window) } if day.OutdoorWindows.Worst != nil { window := *day.OutdoorWindows.Worst window.Daypart = day.Date + " " + window.Daypart planning.WorstWeatherWindows = append(planning.WorstWeatherWindows, window) } for _, daypart := range day.Dayparts { planning.RainStormTiming = appendUnique(planning.RainStormTiming, weekendRainStormNotes(day.Date, daypart)...) planning.ComfortConcerns = appendUnique(planning.ComfortConcerns, weekendComfortNotes(day.Date, daypart)...) } } planning.UncertaintyInputs = appendUnique(planning.UncertaintyInputs, discussion.KeyMessages...) if discussion.ShortTerm != "" { planning.UncertaintyInputs = appendUnique(planning.UncertaintyInputs, "Short-term discussion available for confidence context.") } if discussion.LongTerm != "" { planning.UncertaintyInputs = appendUnique(planning.UncertaintyInputs, "Long-term discussion available for uncertainty context.") } if bundle != nil { for _, warning := range bundle.Warnings { if warning.Code != "" { planning.UncertaintyInputs = appendUnique(planning.UncertaintyInputs, "Source warning: "+warning.Code+".") } } } if len(planning.RainStormTiming) == 0 { planning.RainStormTiming = append(planning.RainStormTiming, "No focused rain or storm timing stands out in the available weekend forecast.") } if len(planning.ComfortConcerns) == 0 { planning.ComfortConcerns = append(planning.ComfortConcerns, "No major heat, cold, or wind comfort concern stands out in the available weekend forecast.") } if len(planning.UncertaintyInputs) == 0 { planning.UncertaintyInputs = append(planning.UncertaintyInputs, "No explicit confidence or uncertainty signal was available from the selected source context.") } return planning } func weekendRainStormNotes(date string, daypart forecast.DaypartSummary) []string { notes := []string{} label := weekendWindowLabel(date, daypart.Name) if daypart.MaxPrecipitationProbability != nil && daypart.MaxPrecipitationProbability.Value >= 30 { notes = append(notes, fmt.Sprintf("%s precipitation chance peaks near %.0f%%.", label, daypart.MaxPrecipitationProbability.Value)) } if daypart.Indicators.Thunder { notes = append(notes, label+" thunder risk is present.") } return notes } func weekendComfortNotes(date string, daypart forecast.DaypartSummary) []string { notes := []string{} label := weekendWindowLabel(date, daypart.Name) if daypart.Indicators.Heat { notes = append(notes, label+" heat may affect outdoor comfort.") } if daypart.Indicators.Cold { notes = append(notes, label+" cold may affect outdoor comfort.") } if daypart.PeakWindGust != nil && daypart.PeakWindGust.Value >= 25 { notes = append(notes, fmt.Sprintf("%s gusts may reach %.0f mph.", label, daypart.PeakWindGust.Value)) } return notes } func weekendWindowLabel(date string, daypart string) string { if daypart == "" { return date } return strings.TrimSpace(date + " " + daypart) }