1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package config
6
7import (
8 "errors"
9 "fmt"
10
11 "github.com/charmbracelet/huh"
12
13 "git.secluded.site/lunatask-mcp-server/internal/config"
14)
15
16func manageAreas(cfg *config.Config) error {
17 nav := manageAreasAsStep(cfg)
18 if nav == navQuit {
19 return errQuit
20 }
21
22 return nil
23}
24
25// manageAreasAsStep runs areas management as a wizard step with Back/Next navigation.
26func manageAreasAsStep(cfg *config.Config) wizardNav {
27 for {
28 options := buildAreaStepOptions(cfg.Areas)
29
30 choice, err := runListSelect(
31 "Areas & Goals",
32 "Areas organize your life (Work, Health, etc). Goals belong to areas.",
33 options,
34 )
35 if err != nil {
36 return navQuit
37 }
38
39 switch choice {
40 case choiceBack:
41 return navBack
42 case choiceNext:
43 return navNext
44 case choiceAdd:
45 if err := addArea(cfg); errors.Is(err, errQuit) {
46 return navQuit
47 }
48 default:
49 idx, ok := parseEditIndex(choice)
50 if !ok {
51 continue
52 }
53
54 if err := manageAreaActions(cfg, idx); errors.Is(err, errQuit) {
55 return navQuit
56 }
57 }
58 }
59}
60
61func buildAreaStepOptions(areas []config.Area) []huh.Option[string] {
62 options := []huh.Option[string]{
63 huh.NewOption("Add new area", choiceAdd),
64 }
65
66 for idx, area := range areas {
67 goalCount := len(area.Goals)
68 label := fmt.Sprintf("%s (%s) - %d goal(s)", area.Name, area.Key, goalCount)
69 options = append(options, huh.NewOption(label, fmt.Sprintf("edit:%d", idx)))
70 }
71
72 options = append(options,
73 huh.NewOption("← Back", choiceBack),
74 huh.NewOption("Next →", choiceNext),
75 )
76
77 return options
78}
79
80func addArea(cfg *config.Config) error {
81 area, err := editArea(nil, cfg)
82 if err != nil {
83 if errors.Is(err, errBack) {
84 return nil
85 }
86
87 return err
88 }
89
90 cfg.Areas = append(cfg.Areas, *area)
91
92 return maybeManageGoals(cfg, len(cfg.Areas)-1)
93}
94
95func manageAreaActions(cfg *config.Config, idx int) error {
96 if idx < 0 || idx >= len(cfg.Areas) {
97 return fmt.Errorf("%w: area %d", errIndexOutRange, idx)
98 }
99
100 area := &cfg.Areas[idx]
101
102 action, err := runActionSelect(fmt.Sprintf("Area: %s (%s)", area.Name, area.Key), true)
103 if err != nil {
104 return err
105 }
106
107 return handleAreaAction(cfg, idx, area, action)
108}
109
110func handleAreaAction(cfg *config.Config, idx int, area *config.Area, action itemAction) error {
111 switch action {
112 case itemActionEdit:
113 updated, err := editArea(area, cfg)
114 if err != nil {
115 if errors.Is(err, errBack) {
116 return nil
117 }
118
119 return err
120 }
121
122 if updated != nil {
123 cfg.Areas[idx] = *updated
124 }
125 case itemActionGoals:
126 return manageGoals(cfg, idx)
127 case itemActionDelete:
128 return deleteArea(cfg, idx)
129 case itemActionNone:
130 // User cancelled or went back
131 }
132
133 return nil
134}
135
136func editArea(existing *config.Area, cfg *config.Config) (*config.Area, error) {
137 area := config.Area{} //nolint:exhaustruct // fields populated by form
138 if existing != nil {
139 area = *existing
140 }
141
142 err := runItemForm(&area.Name, &area.Key, &area.ID, itemFormConfig{
143 itemType: "area",
144 namePlaceholder: "Personal",
145 keyPlaceholder: "personal",
146 keyValidator: validateAreaKey(cfg, existing),
147 })
148 if err != nil {
149 return nil, err
150 }
151
152 return &area, nil
153}
154
155func validateAreaKey(cfg *config.Config, existing *config.Area) func(string) error {
156 return func(input string) error {
157 if err := validateKeyFormat(input); err != nil {
158 return err
159 }
160
161 for idx := range cfg.Areas {
162 if existing != nil && &cfg.Areas[idx] == existing {
163 continue
164 }
165
166 if cfg.Areas[idx].Key == input {
167 return errKeyDuplicate
168 }
169 }
170
171 return nil
172 }
173}
174
175func maybeManageGoals(cfg *config.Config, areaIdx int) error {
176 var manage bool
177
178 err := huh.NewConfirm().
179 Title("Add goals for this area?").
180 Affirmative("Yes").
181 Negative("Not now").
182 Value(&manage).
183 Run()
184 if err != nil {
185 if errors.Is(err, huh.ErrUserAborted) {
186 return errQuit
187 }
188
189 return err
190 }
191
192 if manage {
193 return manageGoals(cfg, areaIdx)
194 }
195
196 return nil
197}
198
199func manageGoals(cfg *config.Config, areaIdx int) error {
200 if areaIdx < 0 || areaIdx >= len(cfg.Areas) {
201 return fmt.Errorf("%w: area %d", errIndexOutRange, areaIdx)
202 }
203
204 area := &cfg.Areas[areaIdx]
205
206 for {
207 options := buildGoalOptions(area.Goals)
208
209 choice, err := runListSelect(
210 fmt.Sprintf("Goals for: %s (%s)", area.Name, area.Key),
211 "",
212 options,
213 )
214 if err != nil {
215 return err
216 }
217
218 if choice == choiceDone {
219 return nil
220 }
221
222 if choice == choiceAdd {
223 if err := addGoal(area); err != nil {
224 return err
225 }
226
227 continue
228 }
229
230 idx, ok := parseEditIndex(choice)
231 if !ok {
232 continue
233 }
234
235 if err := manageGoalActions(area, idx); err != nil {
236 return err
237 }
238 }
239}
240
241func buildGoalOptions(goals []config.Goal) []huh.Option[string] {
242 options := []huh.Option[string]{
243 huh.NewOption("Add new goal", choiceAdd),
244 }
245
246 for idx, goal := range goals {
247 label := fmt.Sprintf("%s (%s)", goal.Name, goal.Key)
248 options = append(options, huh.NewOption(label, fmt.Sprintf("edit:%d", idx)))
249 }
250
251 options = append(options, huh.NewOption("Done", choiceDone))
252
253 return options
254}
255
256func addGoal(area *config.Area) error {
257 goal, err := editGoal(nil, area)
258 if err != nil {
259 if errors.Is(err, errBack) {
260 return nil
261 }
262
263 return err
264 }
265
266 area.Goals = append(area.Goals, *goal)
267
268 return nil
269}
270
271func manageGoalActions(area *config.Area, idx int) error {
272 if idx < 0 || idx >= len(area.Goals) {
273 return fmt.Errorf("%w: goal %d", errIndexOutRange, idx)
274 }
275
276 goal := &area.Goals[idx]
277
278 action, err := runActionSelect(fmt.Sprintf("Goal: %s (%s)", goal.Name, goal.Key), false)
279 if err != nil {
280 return err
281 }
282
283 switch action {
284 case itemActionEdit:
285 updated, err := editGoal(goal, area)
286 if err != nil {
287 if errors.Is(err, errBack) {
288 return nil
289 }
290
291 return err
292 }
293
294 if updated != nil {
295 area.Goals[idx] = *updated
296 }
297 case itemActionDelete:
298 return deleteGoal(area, idx)
299 case itemActionNone, itemActionGoals:
300 // User cancelled or went back; goals not applicable here
301 }
302
303 return nil
304}
305
306func editGoal(existing *config.Goal, area *config.Area) (*config.Goal, error) {
307 goal := config.Goal{} //nolint:exhaustruct // fields populated by form
308 if existing != nil {
309 goal = *existing
310 }
311
312 err := runItemForm(&goal.Name, &goal.Key, &goal.ID, itemFormConfig{
313 itemType: "goal",
314 namePlaceholder: "Learn Gaelic",
315 keyPlaceholder: "gaelic",
316 keyValidator: validateGoalKey(area, existing),
317 })
318 if err != nil {
319 return nil, err
320 }
321
322 return &goal, nil
323}
324
325func validateGoalKey(area *config.Area, existing *config.Goal) func(string) error {
326 return func(input string) error {
327 if err := validateKeyFormat(input); err != nil {
328 return err
329 }
330
331 for idx := range area.Goals {
332 if existing != nil && &area.Goals[idx] == existing {
333 continue
334 }
335
336 if area.Goals[idx].Key == input {
337 return errKeyDuplicate
338 }
339 }
340
341 return nil
342 }
343}
344
345func deleteArea(cfg *config.Config, idx int) error {
346 if idx < 0 || idx >= len(cfg.Areas) {
347 return fmt.Errorf("%w: area %d", errIndexOutRange, idx)
348 }
349
350 area := cfg.Areas[idx]
351
352 var confirm bool
353
354 err := huh.NewConfirm().
355 Title(fmt.Sprintf("Delete area '%s'?", area.Name)).
356 Description(fmt.Sprintf("This will also remove %d goal(s). This cannot be undone.", len(area.Goals))).
357 Affirmative("Delete").
358 Negative("Cancel").
359 Value(&confirm).
360 Run()
361 if err != nil {
362 if errors.Is(err, huh.ErrUserAborted) {
363 return errQuit
364 }
365
366 return err
367 }
368
369 if confirm {
370 cfg.Areas = append(cfg.Areas[:idx], cfg.Areas[idx+1:]...)
371 }
372
373 return nil
374}
375
376func deleteGoal(area *config.Area, idx int) error {
377 if idx < 0 || idx >= len(area.Goals) {
378 return fmt.Errorf("%w: goal %d", errIndexOutRange, idx)
379 }
380
381 goal := area.Goals[idx]
382
383 var confirm bool
384
385 err := huh.NewConfirm().
386 Title(fmt.Sprintf("Delete goal '%s'?", goal.Name)).
387 Description("This cannot be undone.").
388 Affirmative("Delete").
389 Negative("Cancel").
390 Value(&confirm).
391 Run()
392 if err != nil {
393 if errors.Is(err, huh.ErrUserAborted) {
394 return errQuit
395 }
396
397 return err
398 }
399
400 if confirm {
401 area.Goals = append(area.Goals[:idx], area.Goals[idx+1:]...)
402 }
403
404 return nil
405}