1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package cli
  6
  7import (
  8	"context"
  9	"errors"
 10	"fmt"
 11
 12	"git.secluded.site/np/internal/db"
 13	"git.secluded.site/np/internal/event"
 14	"git.secluded.site/np/internal/goal"
 15	"git.secluded.site/np/internal/session"
 16	"git.secluded.site/np/internal/task"
 17	"git.secluded.site/np/internal/timeutil"
 18)
 19
 20// Environment aggregates long-lived services needed by CLI commands.
 21type Environment struct {
 22	DB           *db.Database
 23	Clock        timeutil.Clock
 24	SessionStore *session.Store
 25	GoalStore    *goal.Store
 26	TaskStore    *task.Store
 27	EventStore   *event.Store
 28}
 29
 30// OpenEnvironment initialises storage and domain stores for CLI commands.
 31func OpenEnvironment(opts db.Options, clock timeutil.Clock) (*Environment, error) {
 32	database, err := db.Open(opts)
 33	if err != nil {
 34		return nil, fmt.Errorf("cli: open database: %w", err)
 35	}
 36
 37	if clock == nil {
 38		clock = timeutil.UTCClock{}
 39	}
 40
 41	env := &Environment{
 42		DB:           database,
 43		Clock:        clock,
 44		SessionStore: session.NewStore(database, clock),
 45		GoalStore:    goal.NewStore(database, clock),
 46		TaskStore:    task.NewStore(database, clock),
 47		EventStore:   event.NewStore(database, clock),
 48	}
 49	return env, nil
 50}
 51
 52// Close releases resources allocated by the environment.
 53func (e *Environment) Close() error {
 54	if e == nil || e.DB == nil {
 55		return nil
 56	}
 57	return e.DB.Close()
 58}
 59
 60// ActiveSession resolves the active session for path (or parent paths).
 61func (e *Environment) ActiveSession(ctx context.Context, path string) (session.Document, bool, error) {
 62	if e == nil || e.SessionStore == nil {
 63		return session.Document{}, false, errors.New("cli: environment not initialised")
 64	}
 65	return e.SessionStore.ActiveByPath(ctx, path)
 66}
 67
 68// LoadGoal retrieves the goal for sid. When absent, ok is false.
 69func (e *Environment) LoadGoal(ctx context.Context, sid string) (goal.Document, bool, error) {
 70	if e == nil || e.GoalStore == nil {
 71		return goal.Document{}, false, errors.New("cli: environment not initialised")
 72	}
 73	doc, err := e.GoalStore.Get(ctx, sid)
 74	if err != nil {
 75		if errors.Is(err, goal.ErrNotFound) {
 76			return goal.Document{}, false, nil
 77		}
 78		return goal.Document{}, false, err
 79	}
 80	return doc, true, nil
 81}
 82
 83// LoadTasks returns tasks for sid sorted by creation order.
 84func (e *Environment) LoadTasks(ctx context.Context, sid string) ([]task.Task, error) {
 85	if e == nil || e.TaskStore == nil {
 86		return nil, errors.New("cli: environment not initialised")
 87	}
 88	return e.TaskStore.List(ctx, sid)
 89}
 90
 91// LoadTasksByStatus returns tasks filtered by status. When status is empty,
 92// all tasks are returned.
 93func (e *Environment) LoadTasksByStatus(ctx context.Context, sid string, status task.Status) ([]task.Task, error) {
 94	if e == nil || e.TaskStore == nil {
 95		return nil, errors.New("cli: environment not initialised")
 96	}
 97	if status == "" {
 98		return e.TaskStore.List(ctx, sid)
 99	}
100	return e.TaskStore.ListByStatus(ctx, sid, status)
101}
102
103type contextKey string
104
105const environmentContextKey contextKey = "git.secluded.site/np/internal/cli/environment"
106
107// WithEnvironment stores env inside ctx for retrieval by subcommands.
108func WithEnvironment(ctx context.Context, env *Environment) context.Context {
109	return context.WithValue(ctx, environmentContextKey, env)
110}
111
112// FromContext extracts an Environment that was previously embedded with WithEnvironment.
113func FromContext(ctx context.Context) (*Environment, bool) {
114	if ctx == nil {
115		return nil, false
116	}
117	env, ok := ctx.Value(environmentContextKey).(*Environment)
118	if !ok || env == nil {
119		return nil, false
120	}
121	return env, true
122}