1package services
2
3import (
4 "context"
5 "errors"
6 "time"
7
8 "github.com/bnema/archup/internal/application/commands"
9 "github.com/bnema/archup/internal/application/dto"
10 "github.com/bnema/archup/internal/application/handlers"
11 "github.com/bnema/archup/internal/domain/installation"
12 "github.com/bnema/archup/internal/domain/ports"
13)
14
15// InstallationService orchestrates the entire installation process
16type InstallationService struct {
17 // Domain
18 installAgg *installation.Installation
19
20 // Handlers
21 bootstrapHandler *handlers.BootstrapHandler
22 preflightHandler *handlers.PreflightHandler
23 partitionHandler *handlers.PartitionHandler
24 baseHandler *handlers.InstallBaseHandler
25 configHandler *handlers.ConfigureSystemHandler
26 bootloaderHandler *handlers.BootloaderHandler
27 reposHandler *handlers.ReposHandler
28 postInstallHandler *handlers.PostInstallHandler
29
30 // Ports
31 repo ports.InstallationRepository
32 logger ports.Logger
33
34 // Progress tracking
35 tracker *ProgressTracker
36
37 // State
38 startTime time.Time
39}
40
41// NewInstallationService creates a new installation service with all handlers
42func NewInstallationService(
43 repo ports.InstallationRepository,
44 logger ports.Logger,
45 bootstrapHandler *handlers.BootstrapHandler,
46 preflightHandler *handlers.PreflightHandler,
47 partitionHandler *handlers.PartitionHandler,
48 baseHandler *handlers.InstallBaseHandler,
49 configHandler *handlers.ConfigureSystemHandler,
50 bootloaderHandler *handlers.BootloaderHandler,
51 reposHandler *handlers.ReposHandler,
52 postInstallHandler *handlers.PostInstallHandler,
53) *InstallationService {
54 return &InstallationService{
55 repo: repo,
56 logger: logger,
57 bootstrapHandler: bootstrapHandler,
58 preflightHandler: preflightHandler,
59 partitionHandler: partitionHandler,
60 baseHandler: baseHandler,
61 configHandler: configHandler,
62 bootloaderHandler: bootloaderHandler,
63 reposHandler: reposHandler,
64 postInstallHandler: postInstallHandler,
65 tracker: NewProgressTracker(),
66 }
67}
68
69// Start begins a new installation
70func (s *InstallationService) Start(ctx context.Context, hostname string, username string, targetDisk string, encryptionType string) error {
71 s.logger.Info("Initializing installation", "hostname", hostname, "username", username)
72
73 // Create installation aggregate
74 inst, err := installation.NewInstallation(hostname, username, targetDisk, encryptionType)
75 if err != nil {
76 s.logger.Error("Failed to create installation", "error", err)
77 s.tracker.EmitPhaseError("initialization", 0, 8, err.Error())
78 return err
79 }
80
81 s.installAgg = inst
82 s.startTime = time.Now()
83
84 // Start the installation
85 if err := s.installAgg.Start(ctx); err != nil {
86 s.logger.Error("Failed to start installation", "error", err)
87 s.tracker.EmitPhaseError("initialization", 0, 8, err.Error())
88 return err
89 }
90
91 return nil
92}
93
94// RunPreflight runs preflight checks
95func (s *InstallationService) RunPreflight(ctx context.Context) (*dto.PreflightResult, error) {
96 if s.installAgg == nil {
97 return nil, errors.New("installation not started")
98 }
99
100 s.tracker.EmitPhaseStarted("Preflight Checks", 1, 8)
101
102 result, err := s.preflightHandler.Handle(ctx, commands.PreflightCommand{})
103 if err != nil {
104 s.tracker.EmitPhaseError("Preflight Checks", 1, 8, err.Error())
105 return nil, err
106 }
107
108 if !result.ChecksPassed {
109 errMsg := "Preflight checks failed"
110 if len(result.CriticalErrors) > 0 {
111 errMsg = result.CriticalErrors[0]
112 }
113 s.tracker.EmitPhaseError("Preflight Checks", 1, 8, errMsg)
114 return result, errors.New(errMsg)
115 }
116
117 s.tracker.EmitPhaseCompleted("Preflight Checks", 1, 8)
118 return result, nil
119}
120
121// RunBootstrap downloads/clones install files
122func (s *InstallationService) RunBootstrap(ctx context.Context) (*dto.BootstrapResult, error) {
123 s.logger.Info("Running bootstrap to prepare install files")
124
125 result, err := s.bootstrapHandler.Handle(ctx)
126 if err != nil {
127 s.logger.Error("Bootstrap failed", "error", err)
128 return result, err
129 }
130
131 s.logger.Info("Bootstrap completed", "method", result.Method)
132 return result, nil
133}
134
135// RunPartition runs disk partitioning
136func (s *InstallationService) RunPartition(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) {
137 if s.installAgg == nil {
138 return nil, errors.New("installation not started")
139 }
140
141 s.tracker.EmitPhaseStarted("Disk Partitioning", 2, 8)
142
143 result, err := s.partitionHandler.Handle(ctx, cmd)
144 if err != nil {
145 s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, err.Error())
146 return nil, err
147 }
148
149 if !result.Success {
150 s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, result.ErrorDetail)
151 return result, errors.New(result.ErrorDetail)
152 }
153
154 s.tracker.EmitPhaseCompleted("Disk Partitioning", 2, 8)
155 return result, nil
156}
157
158// RunBaseInstall runs base system installation
159func (s *InstallationService) RunBaseInstall(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) {
160 if s.installAgg == nil {
161 return nil, errors.New("installation not started")
162 }
163
164 s.tracker.EmitPhaseStarted("Base Installation", 3, 8)
165
166 result, err := s.baseHandler.Handle(ctx, cmd)
167 if err != nil {
168 s.tracker.EmitPhaseError("Base Installation", 3, 8, err.Error())
169 return nil, err
170 }
171
172 if !result.Success {
173 s.tracker.EmitPhaseError("Base Installation", 3, 8, result.ErrorDetail)
174 return result, errors.New(result.ErrorDetail)
175 }
176
177 s.tracker.EmitPhaseCompleted("Base Installation", 3, 8)
178 return result, nil
179}
180
181// RunConfigSystem runs system configuration
182func (s *InstallationService) RunConfigSystem(ctx context.Context, cmd commands.ConfigureSystemCommand) (*dto.ConfigureSystemResult, error) {
183 if s.installAgg == nil {
184 return nil, errors.New("installation not started")
185 }
186
187 s.tracker.EmitPhaseStarted("System Configuration", 4, 8)
188
189 result, err := s.configHandler.Handle(ctx, cmd)
190 if err != nil {
191 s.tracker.EmitPhaseError("System Configuration", 4, 8, err.Error())
192 return nil, err
193 }
194
195 if !result.Success {
196 s.tracker.EmitPhaseError("System Configuration", 4, 8, result.ErrorDetail)
197 return result, errors.New(result.ErrorDetail)
198 }
199
200 s.tracker.EmitPhaseCompleted("System Configuration", 4, 8)
201 return result, nil
202}
203
204// RunBootloaderSetup runs bootloader setup
205func (s *InstallationService) RunBootloaderSetup(ctx context.Context, cmd commands.InstallBootloaderCommand) (*dto.BootloaderResult, error) {
206 if s.installAgg == nil {
207 return nil, errors.New("installation not started")
208 }
209
210 s.tracker.EmitPhaseStarted("Bootloader Setup", 5, 8)
211
212 result, err := s.bootloaderHandler.Handle(ctx, cmd)
213 if err != nil {
214 s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, err.Error())
215 return nil, err
216 }
217
218 if !result.Success {
219 s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, result.ErrorDetail)
220 return result, errors.New(result.ErrorDetail)
221 }
222
223 s.tracker.EmitPhaseCompleted("Bootloader Setup", 5, 8)
224 return result, nil
225}
226
227// RunRepositorySetup runs repository setup
228func (s *InstallationService) RunRepositorySetup(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) {
229 if s.installAgg == nil {
230 return nil, errors.New("installation not started")
231 }
232
233 s.tracker.EmitPhaseStarted("Repository Setup", 6, 8)
234
235 result, err := s.reposHandler.Handle(ctx, cmd)
236 if err != nil {
237 s.tracker.EmitPhaseError("Repository Setup", 6, 8, err.Error())
238 return nil, err
239 }
240
241 if !result.Success {
242 s.tracker.EmitPhaseError("Repository Setup", 6, 8, result.ErrorDetail)
243 return result, errors.New(result.ErrorDetail)
244 }
245
246 s.tracker.EmitPhaseCompleted("Repository Setup", 6, 8)
247 return result, nil
248}
249
250// RunPostInstall runs post-installation tasks
251func (s *InstallationService) RunPostInstall(ctx context.Context, cmd commands.PostInstallCommand) (*dto.PostInstallResult, error) {
252 if s.installAgg == nil {
253 return nil, errors.New("installation not started")
254 }
255
256 s.tracker.EmitPhaseStarted("Post-Installation", 7, 8)
257
258 result, err := s.postInstallHandler.Handle(ctx, cmd)
259 if err != nil {
260 s.tracker.EmitPhaseError("Post-Installation", 7, 8, err.Error())
261 return nil, err
262 }
263
264 if !result.Success {
265 s.tracker.EmitPhaseError("Post-Installation", 7, 8, result.ErrorDetail)
266 return result, errors.New(result.ErrorDetail)
267 }
268
269 s.tracker.EmitPhaseCompleted("Post-Installation", 7, 8)
270 return result, nil
271}
272
273// Complete marks installation as complete
274func (s *InstallationService) Complete(ctx context.Context) error {
275 if s.installAgg == nil {
276 return errors.New("installation not started")
277 }
278
279 duration := int(time.Since(s.startTime).Seconds())
280 if err := s.installAgg.Complete(duration); err != nil {
281 s.logger.Error("Failed to complete installation", "error", err)
282 s.tracker.EmitPhaseError("Completion", 8, 8, err.Error())
283 return err
284 }
285
286 s.tracker.EmitPhaseCompleted("Completion", 8, 8)
287 s.logger.Info("Installation completed successfully", "duration", duration)
288
289 return nil
290}
291
292// GetStatus returns the current installation status
293func (s *InstallationService) GetStatus() *dto.InstallationStatus {
294 if s.installAgg == nil {
295 return &dto.InstallationStatus{
296 State: "NotStarted",
297 }
298 }
299
300 completedAt := s.installAgg.CompletedAt()
301 startedAt := s.installAgg.StartedAt()
302
303 return &dto.InstallationStatus{
304 ID: s.installAgg.ID(),
305 State: s.installAgg.State().String(),
306 Hostname: s.installAgg.Hostname(),
307 Username: s.installAgg.Username(),
308 TargetDisk: s.installAgg.TargetDisk(),
309 EncryptionType: s.installAgg.EncryptionType(),
310 Progress: s.installAgg.ProgressPercentage(),
311 StartedAt: startedAt,
312 CompletedAt: completedAt,
313 CurrentPhase: s.installAgg.State().String(),
314 }
315}
316
317// SubscribeProgress returns a channel for progress updates
318func (s *InstallationService) SubscribeProgress() <-chan *dto.ProgressUpdate {
319 return s.tracker.Subscribe()
320}
321
322// Tracker returns the progress tracker (for TUI initialization)
323func (s *InstallationService) Tracker() *ProgressTracker {
324 return s.tracker
325}
326
327// Close closes the service
328func (s *InstallationService) Close() error {
329 s.tracker.Close()
330 return nil
331}