1package installation
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "github.com/google/uuid"
10)
11
12// Installation is the aggregate root entity representing the entire installation process
13// It manages the lifecycle of an Arch Linux installation from start to completion
14type Installation struct {
15 // Unique identifier for this installation
16 id string
17
18 // Current state of the installation
19 state State
20
21 // Configuration for the installation (hostname, username, etc.)
22 // These are immutable once set
23 hostname string
24 username string
25 targetDisk string
26 encryptionType string
27
28 // Installation metadata
29 createdAt time.Time
30 startedAt *time.Time
31 completedAt *time.Time
32
33 // Domain events that have occurred during this installation
34 events []DomainEvent
35}
36
37// NewInstallation creates a new Installation aggregate
38// All parameters are validated according to business rules
39func NewInstallation(
40 hostname string,
41 username string,
42 targetDisk string,
43 encryptionType string,
44) (*Installation, error) {
45 // Validate hostname
46 if hostname == "" {
47 return nil, errors.New("hostname cannot be empty")
48 }
49 if len(hostname) > 63 {
50 return nil, errors.New("hostname cannot exceed 63 characters")
51 }
52
53 // Validate username
54 if username == "" {
55 return nil, errors.New("username cannot be empty")
56 }
57 if len(username) > 32 {
58 return nil, errors.New("username cannot exceed 32 characters")
59 }
60
61 // Validate targetDisk
62 if targetDisk == "" {
63 return nil, errors.New("target disk cannot be empty")
64 }
65
66 // Validate encryptionType
67 validEncryption := map[string]bool{
68 "none": true,
69 "luks": true,
70 "luks-lvm": true,
71 }
72 if !validEncryption[encryptionType] {
73 return nil, fmt.Errorf("invalid encryption type: %s", encryptionType)
74 }
75
76 installation := &Installation{
77 id: uuid.New().String(),
78 state: StateNotStarted,
79 hostname: hostname,
80 username: username,
81 targetDisk: targetDisk,
82 encryptionType: encryptionType,
83 createdAt: time.Now().UTC(),
84 events: []DomainEvent{},
85 }
86
87 return installation, nil
88}
89
90// ID returns the unique identifier
91func (i *Installation) ID() string {
92 return i.id
93}
94
95// State returns the current installation state
96func (i *Installation) State() State {
97 return i.state
98}
99
100// Hostname returns the configured hostname
101func (i *Installation) Hostname() string {
102 return i.hostname
103}
104
105// Username returns the configured username
106func (i *Installation) Username() string {
107 return i.username
108}
109
110// TargetDisk returns the target installation disk
111func (i *Installation) TargetDisk() string {
112 return i.targetDisk
113}
114
115// EncryptionType returns the encryption type ("none", "luks", or "luks-lvm")
116func (i *Installation) EncryptionType() string {
117 return i.encryptionType
118}
119
120// CreatedAt returns when this installation was created
121func (i *Installation) CreatedAt() time.Time {
122 return i.createdAt
123}
124
125// StartedAt returns when the installation actually started (or nil if not started)
126func (i *Installation) StartedAt() *time.Time {
127 return i.startedAt
128}
129
130// CompletedAt returns when the installation completed (or nil if not completed)
131func (i *Installation) CompletedAt() *time.Time {
132 return i.completedAt
133}
134
135// IsStarted returns true if the installation has been started
136func (i *Installation) IsStarted() bool {
137 return i.state != StateNotStarted
138}
139
140// IsCompleted returns true if the installation has completed (successfully or failed)
141func (i *Installation) IsCompleted() bool {
142 return i.state.IsTerminal()
143}
144
145// IsSuccessful returns true if the installation completed successfully
146func (i *Installation) IsSuccessful() bool {
147 return i.state == StateCompleted
148}
149
150// IsFailed returns true if the installation failed
151func (i *Installation) IsFailed() bool {
152 return i.state == StateFailed
153}
154
155// Start transitions the installation from NotStarted to PreflightChecks
156// This is the entry point that validates prerequisites and begins the process
157func (i *Installation) Start(ctx context.Context) error {
158 if i.state != StateNotStarted {
159 return ErrInstallationAlreadyStarted
160 }
161
162 now := time.Now().UTC()
163 i.startedAt = &now
164
165 // Transition to preflight checks
166 if err := i.state.TransitionTo(StatePreflightChecks); err != nil {
167 return err
168 }
169 i.state = StatePreflightChecks
170
171 // Emit event
172 event := NewInstallationStartedEvent(
173 i.id,
174 i.hostname,
175 i.username,
176 i.targetDisk,
177 i.encryptionType,
178 )
179 i.recordEvent(event)
180
181 return nil
182}
183
184// TransitionToNextPhase moves the installation to the next phase in the sequence
185func (i *Installation) TransitionToNextPhase() error {
186 if i.state.IsTerminal() {
187 return ErrInstallationAlreadyCompleted
188 }
189
190 nextState := i.getNextPhase()
191 if nextState == -1 {
192 return ErrInvalidPhaseTransition
193 }
194
195 if err := i.state.TransitionTo(State(nextState)); err != nil {
196 return err
197 }
198 i.state = State(nextState)
199
200 // Emit event
201 event := NewPhaseStartedEvent(i.id, i.state)
202 i.recordEvent(event)
203
204 return nil
205}
206
207// CompleteCurrentPhase marks the current phase as completed and transitions to next
208func (i *Installation) CompleteCurrentPhase(durationSeconds int) error {
209 if i.state == StateNotStarted || i.state.IsTerminal() {
210 return ErrInstallationNotStarted
211 }
212
213 currentPhase := i.state
214
215 // Record completion event
216 nextPhase := i.getNextPhase()
217 event := NewPhaseCompletedEvent(i.id, currentPhase, State(nextPhase), durationSeconds)
218 i.recordEvent(event)
219
220 // Transition to next phase
221 return i.TransitionToNextPhase()
222}
223
224// FailCurrentPhase marks the current phase as failed
225func (i *Installation) FailCurrentPhase(errorMessage string, recoverable bool) error {
226 if i.state == StateNotStarted {
227 return ErrInstallationNotStarted
228 }
229
230 if i.state.IsTerminal() {
231 return ErrInstallationAlreadyCompleted
232 }
233
234 currentPhase := i.state
235
236 // Record failure event
237 event := NewPhaseFailedEvent(i.id, currentPhase, errorMessage, recoverable)
238 i.recordEvent(event)
239
240 // Transition to failed state
241 if err := i.state.TransitionTo(StateFailed); err != nil {
242 return err
243 }
244 i.state = StateFailed
245
246 // Record installation failure event
247 failureEvent := NewInstallationFailedEvent(i.id, currentPhase, errorMessage)
248 i.recordEvent(failureEvent)
249
250 return nil
251}
252
253// Complete marks the installation as successfully completed
254// Can be called from any non-terminal state (phases are tracked by progress tracker, not state machine)
255func (i *Installation) Complete(totalDurationSeconds int) error {
256 if i.state.IsTerminal() {
257 return ErrInstallationAlreadyCompleted
258 }
259 if i.state == StateNotStarted {
260 return ErrInstallationNotStarted
261 }
262
263 now := time.Now().UTC()
264 i.completedAt = &now
265 i.state = StateCompleted
266
267 // Emit completion event
268 event := NewInstallationCompletedEvent(i.id, totalDurationSeconds)
269 i.recordEvent(event)
270
271 return nil
272}
273
274// ProgressPercentage returns estimated progress as percentage (0-100)
275func (i *Installation) ProgressPercentage() int {
276 return i.state.ProgressPercentage()
277}
278
279// UncommittedEvents returns all domain events that have occurred but not yet persisted
280func (i *Installation) UncommittedEvents() []DomainEvent {
281 return i.events
282}
283
284// ClearEvents clears the uncommitted events after persistence
285func (i *Installation) ClearEvents() {
286 i.events = []DomainEvent{}
287}
288
289// Private methods
290
291// recordEvent adds a domain event to the uncommitted events list
292func (i *Installation) recordEvent(event DomainEvent) {
293 i.events = append(i.events, event)
294}
295
296// getNextPhase returns the next phase in the sequence
297func (i *Installation) getNextPhase() State {
298 sequence := PhaseSequence()
299 currentIndex := PhaseIndex(i.state)
300
301 if currentIndex == -1 || currentIndex >= len(sequence)-1 {
302 return -1
303 }
304
305 return sequence[currentIndex+1]
306}