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 manageHabits(cfg *config.Config) error {
17 nav := manageHabitsAsStep(cfg)
18 if nav == navQuit {
19 return errQuit
20 }
21
22 return nil
23}
24
25// manageHabitsAsStep runs habits management as a wizard step with Back/Next navigation.
26func manageHabitsAsStep(cfg *config.Config) wizardNav {
27 for {
28 options := buildHabitStepOptions(cfg.Habits)
29
30 choice, err := runListSelect(
31 "Habits",
32 "Track habits via the MCP server.",
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 := addHabit(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 := manageHabitActions(cfg, idx); errors.Is(err, errQuit) {
55 return navQuit
56 }
57 }
58 }
59}
60
61func buildHabitStepOptions(habits []config.Habit) []huh.Option[string] {
62 options := []huh.Option[string]{
63 huh.NewOption("Add new habit", choiceAdd),
64 }
65
66 for idx, habit := range habits {
67 label := fmt.Sprintf("%s (%s)", habit.Name, habit.Key)
68 options = append(options, huh.NewOption(label, fmt.Sprintf("edit:%d", idx)))
69 }
70
71 options = append(options,
72 huh.NewOption("← Back", choiceBack),
73 huh.NewOption("Next →", choiceNext),
74 )
75
76 return options
77}
78
79func addHabit(cfg *config.Config) error {
80 habit, err := editHabit(nil, cfg)
81 if err != nil {
82 if errors.Is(err, errBack) {
83 return nil
84 }
85
86 return err
87 }
88
89 cfg.Habits = append(cfg.Habits, *habit)
90
91 return nil
92}
93
94func manageHabitActions(cfg *config.Config, idx int) error {
95 if idx < 0 || idx >= len(cfg.Habits) {
96 return fmt.Errorf("%w: habit %d", errIndexOutRange, idx)
97 }
98
99 habit := &cfg.Habits[idx]
100
101 action, err := runActionSelect(fmt.Sprintf("Habit: %s (%s)", habit.Name, habit.Key), false)
102 if err != nil {
103 return err
104 }
105
106 switch action {
107 case itemActionEdit:
108 updated, err := editHabit(habit, cfg)
109 if err != nil {
110 if errors.Is(err, errBack) {
111 return nil
112 }
113
114 return err
115 }
116
117 if updated != nil {
118 cfg.Habits[idx] = *updated
119 }
120 case itemActionDelete:
121 return deleteHabit(cfg, idx)
122 case itemActionNone, itemActionGoals:
123 // User cancelled or went back; goals not applicable here
124 }
125
126 return nil
127}
128
129func editHabit(existing *config.Habit, cfg *config.Config) (*config.Habit, error) {
130 habit := config.Habit{} //nolint:exhaustruct // fields populated by form
131 if existing != nil {
132 habit = *existing
133 }
134
135 err := runItemForm(&habit.Name, &habit.Key, &habit.ID, itemFormConfig{
136 itemType: "habit",
137 namePlaceholder: "Study Gaelic",
138 keyPlaceholder: "gaelic",
139 keyValidator: validateHabitKey(cfg, existing),
140 })
141 if err != nil {
142 return nil, err
143 }
144
145 return &habit, nil
146}
147
148func validateHabitKey(cfg *config.Config, existing *config.Habit) func(string) error {
149 return func(input string) error {
150 if err := validateKeyFormat(input); err != nil {
151 return err
152 }
153
154 for idx := range cfg.Habits {
155 if existing != nil && &cfg.Habits[idx] == existing {
156 continue
157 }
158
159 if cfg.Habits[idx].Key == input {
160 return errKeyDuplicate
161 }
162 }
163
164 return nil
165 }
166}
167
168func deleteHabit(cfg *config.Config, idx int) error {
169 if idx < 0 || idx >= len(cfg.Habits) {
170 return fmt.Errorf("%w: habit %d", errIndexOutRange, idx)
171 }
172
173 habit := cfg.Habits[idx]
174
175 var confirm bool
176
177 err := huh.NewConfirm().
178 Title(fmt.Sprintf("Delete habit '%s'?", habit.Name)).
179 Description("This cannot be undone.").
180 Affirmative("Delete").
181 Negative("Cancel").
182 Value(&confirm).
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 confirm {
193 cfg.Habits = append(cfg.Habits[:idx], cfg.Habits[idx+1:]...)
194 }
195
196 return nil
197}