package engine import ( "fmt" "time" "github.com/google/uuid" ) type Status byte const ( StatusPending Status = 'P' StatusCompleted Status = 'C' StatusDeleted Status = 'D' StatusRecurring Status = 'R' ) type Priority int const ( PriorityLow Priority = 0 PriorityDefault Priority = 1 PriorityMedium Priority = 2 PriorityHigh Priority = 3 ) type Task struct { // Identity UUID uuid.UUID ID int // Core fields Status Status Description string Project *string Priority Priority // Timestamps Created time.Time Modified time.Time Start *time.Time End *time.Time Due *time.Time Scheduled *time.Time Wait *time.Time Until *time.Time // Recurrence (parent-child approach) RecurrenceDuration *time.Duration ParentUUID *uuid.UUID // Derived fields (not stored in DB) Tags []string } // timeNow returns current time (allows mocking in tests) var timeNow = time.Now // Helper functions for SQL time conversion func timeToSQL(t *time.Time) interface{} { if t == nil { return nil } return t.Unix() } func sqlToTime(v interface{}) *time.Time { if v == nil { return nil } unix, ok := v.(int64) if !ok { return nil } t := time.Unix(unix, 0) return &t } func durationToSQL(d *time.Duration) interface{} { if d == nil { return nil } return int64(*d) } func sqlToDuration(v interface{}) *time.Duration { if v == nil { return nil } nanos, ok := v.(int64) if !ok { return nil } d := time.Duration(nanos) return &d } func stringPtrToSQL(s *string) interface{} { if s == nil { return nil } return *s } func sqlToStringPtr(v interface{}) *string { if v == nil { return nil } str, ok := v.(string) if !ok { return nil } return &str } func uuidPtrToSQL(u *uuid.UUID) interface{} { if u == nil { return nil } return u.String() } func sqlToUUIDPtr(v interface{}) *uuid.UUID { if v == nil { return nil } str, ok := v.(string) if !ok { return nil } u, err := uuid.Parse(str) if err != nil { return nil } return &u } // CreateTask creates a new task with the given description func CreateTask(description string) (*Task, error) { return CreateTaskWithModifier(description, nil) } // CreateTaskWithModifier creates a new task with the given description and applies modifiers func CreateTaskWithModifier(description string, mod *Modifier) (*Task, error) { now := timeNow() task := &Task{ UUID: uuid.New(), Status: StatusPending, Description: description, Priority: PriorityDefault, Created: now, Modified: now, Tags: []string{}, } // Apply modifiers before saving (for attributes) if mod != nil { if err := mod.ApplyToNew(task); err != nil { return nil, err } } if err := task.Save(); err != nil { return nil, err } // Apply tags after saving (requires task.ID) if mod != nil { for _, tag := range mod.AddTags { if err := task.AddTag(tag); err != nil { return nil, err } } } return task, nil } // GetTask retrieves a task by UUID func GetTask(taskUUID uuid.UUID) (*Task, error) { db := GetDB() if db == nil { return nil, fmt.Errorf("database not initialized") } query := ` SELECT id, uuid, status, description, project, priority, created, modified, start, end, due, scheduled, wait, until_date, recurrence_duration, parent_uuid FROM tasks WHERE uuid = ? ` task := &Task{} var ( uuidStr string project interface{} created int64 modified int64 start interface{} end interface{} due interface{} scheduled interface{} wait interface{} until interface{} recurDuration interface{} parentUUIDStr interface{} ) err := db.QueryRow(query, taskUUID.String()).Scan( &task.ID, &uuidStr, &task.Status, &task.Description, &project, &task.Priority, &created, &modified, &start, &end, &due, &scheduled, &wait, &until, &recurDuration, &parentUUIDStr, ) if err != nil { return nil, fmt.Errorf("failed to get task: %w", err) } // Parse UUID task.UUID, err = uuid.Parse(uuidStr) if err != nil { return nil, fmt.Errorf("failed to parse UUID: %w", err) } // Convert timestamps task.Created = time.Unix(created, 0) task.Modified = time.Unix(modified, 0) // Convert nullable fields task.Project = sqlToStringPtr(project) task.Start = sqlToTime(start) task.End = sqlToTime(end) task.Due = sqlToTime(due) task.Scheduled = sqlToTime(scheduled) task.Wait = sqlToTime(wait) task.Until = sqlToTime(until) task.RecurrenceDuration = sqlToDuration(recurDuration) task.ParentUUID = sqlToUUIDPtr(parentUUIDStr) // Load tags tags, err := task.GetTags() if err != nil { return nil, fmt.Errorf("failed to load tags: %w", err) } task.Tags = tags return task, nil } // GetTasks retrieves all tasks with optional filtering func GetTasks(filter *Filter) ([]*Task, error) { db := GetDB() if db == nil { return nil, fmt.Errorf("database not initialized") } // Build WHERE clause from filter whereClause := "1=1" var args []interface{} if filter != nil { whereClause, args = filter.ToSQL() } query := fmt.Sprintf(` SELECT id, uuid, status, description, project, priority, created, modified, start, end, due, scheduled, wait, until_date, recurrence_duration, parent_uuid FROM tasks WHERE %s ORDER BY CASE WHEN due IS NULL THEN 1 ELSE 0 END, due ASC, priority DESC `, whereClause) rows, err := db.Query(query, args...) if err != nil { return nil, fmt.Errorf("failed to query tasks: %w", err) } defer rows.Close() tasks := []*Task{} for rows.Next() { task := &Task{} var ( uuidStr string project interface{} created int64 modified int64 start interface{} end interface{} due interface{} scheduled interface{} wait interface{} until interface{} recurDuration interface{} parentUUIDStr interface{} ) err := rows.Scan( &task.ID, &uuidStr, &task.Status, &task.Description, &project, &task.Priority, &created, &modified, &start, &end, &due, &scheduled, &wait, &until, &recurDuration, &parentUUIDStr, ) if err != nil { return nil, fmt.Errorf("failed to scan task: %w", err) } // Parse UUID task.UUID, err = uuid.Parse(uuidStr) if err != nil { return nil, fmt.Errorf("failed to parse UUID: %w", err) } // Convert timestamps task.Created = time.Unix(created, 0) task.Modified = time.Unix(modified, 0) // Convert nullable fields task.Project = sqlToStringPtr(project) task.Start = sqlToTime(start) task.End = sqlToTime(end) task.Due = sqlToTime(due) task.Scheduled = sqlToTime(scheduled) task.Wait = sqlToTime(wait) task.Until = sqlToTime(until) task.RecurrenceDuration = sqlToDuration(recurDuration) task.ParentUUID = sqlToUUIDPtr(parentUUIDStr) // Load tags tags, err := task.GetTags() if err != nil { return nil, fmt.Errorf("failed to load tags: %w", err) } task.Tags = tags tasks = append(tasks, task) } return tasks, nil } // Save inserts or updates the task in the database func (t *Task) Save() error { db := GetDB() if db == nil { return fmt.Errorf("database not initialized") } t.Modified = timeNow() // Check if task exists var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM tasks WHERE uuid = ?)", t.UUID.String()).Scan(&exists) if err != nil { return fmt.Errorf("failed to check if task exists: %w", err) } if exists { // Update existing task query := ` UPDATE tasks SET status = ?, description = ?, project = ?, priority = ?, modified = ?, start = ?, end = ?, due = ?, scheduled = ?, wait = ?, until_date = ?, recurrence_duration = ?, parent_uuid = ? WHERE uuid = ? ` _, err = db.Exec(query, t.Status, t.Description, stringPtrToSQL(t.Project), t.Priority, t.Modified.Unix(), timeToSQL(t.Start), timeToSQL(t.End), timeToSQL(t.Due), timeToSQL(t.Scheduled), timeToSQL(t.Wait), timeToSQL(t.Until), durationToSQL(t.RecurrenceDuration), uuidPtrToSQL(t.ParentUUID), t.UUID.String(), ) if err != nil { return fmt.Errorf("failed to update task: %w", err) } } else { // Insert new task query := ` INSERT INTO tasks ( uuid, status, description, project, priority, created, modified, start, end, due, scheduled, wait, until_date, recurrence_duration, parent_uuid ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` result, err := db.Exec(query, t.UUID.String(), t.Status, t.Description, stringPtrToSQL(t.Project), t.Priority, t.Created.Unix(), t.Modified.Unix(), timeToSQL(t.Start), timeToSQL(t.End), timeToSQL(t.Due), timeToSQL(t.Scheduled), timeToSQL(t.Wait), timeToSQL(t.Until), durationToSQL(t.RecurrenceDuration), uuidPtrToSQL(t.ParentUUID), ) if err != nil { return fmt.Errorf("failed to insert task: %w", err) } id, err := result.LastInsertId() if err != nil { return fmt.Errorf("failed to get last insert id: %w", err) } t.ID = int(id) } return nil } // AddTag adds a tag to the task func (t *Task) AddTag(tag string) error { db := GetDB() if db == nil { return fmt.Errorf("database not initialized") } // Ensure task has been saved and has an ID if t.ID == 0 { return fmt.Errorf("task must be saved before adding tags") } _, err := db.Exec("INSERT OR IGNORE INTO tags (task_id, tag) VALUES (?, ?)", t.ID, tag) if err != nil { return fmt.Errorf("failed to add tag: %w", err) } // Update in-memory tags for _, existingTag := range t.Tags { if existingTag == tag { return nil // Already exists } } t.Tags = append(t.Tags, tag) return nil } // RemoveTag removes a tag from the task func (t *Task) RemoveTag(tag string) error { db := GetDB() if db == nil { return fmt.Errorf("database not initialized") } if t.ID == 0 { return fmt.Errorf("task must be saved before removing tags") } _, err := db.Exec("DELETE FROM tags WHERE task_id = ? AND tag = ?", t.ID, tag) if err != nil { return fmt.Errorf("failed to remove tag: %w", err) } // Update in-memory tags newTags := []string{} for _, existingTag := range t.Tags { if existingTag != tag { newTags = append(newTags, existingTag) } } t.Tags = newTags return nil } // GetTags retrieves all tags for the task func (t *Task) GetTags() ([]string, error) { db := GetDB() if db == nil { return nil, fmt.Errorf("database not initialized") } if t.ID == 0 { return []string{}, nil } rows, err := db.Query("SELECT tag FROM tags WHERE task_id = ? ORDER BY tag", t.ID) if err != nil { return nil, fmt.Errorf("failed to get tags: %w", err) } defer rows.Close() tags := []string{} for rows.Next() { var tag string if err := rows.Scan(&tag); err != nil { return nil, fmt.Errorf("failed to scan tag: %w", err) } tags = append(tags, tag) } return tags, nil } // Complete marks a task as completed func (t *Task) Complete() error { t.Status = StatusCompleted now := timeNow() t.End = &now if err := t.Save(); err != nil { return err } // If this is a recurring instance, spawn next instance if t.ParentUUID != nil { if err := SpawnNextInstance(t); err != nil { // Log error but don't fail the completion return fmt.Errorf("completed task but failed to spawn next instance: %w", err) } } return nil } // Delete marks a task as deleted (soft delete) func (t *Task) Delete(permanent bool) error { db := GetDB() if db == nil { return fmt.Errorf("database not initialized") } if permanent { // Hard delete - remove from database _, err := db.Exec("DELETE FROM tasks WHERE uuid = ?", t.UUID.String()) if err != nil { return fmt.Errorf("failed to delete task: %w", err) } } else { // Soft delete - mark as deleted t.Status = StatusDeleted now := timeNow() t.End = &now if err := t.Save(); err != nil { return err } } return nil } // StartTask sets the start time for a task func (t *Task) StartTask() error { now := timeNow() t.Start = &now return t.Save() } // StopTask clears the start time for a task func (t *Task) StopTask() error { t.Start = nil return t.Save() } // IsRecurringTemplate returns true if task is a recurring template func (t *Task) IsRecurringTemplate() bool { return t.Status == StatusRecurring && t.RecurrenceDuration != nil && t.ParentUUID == nil } // IsRecurringInstance returns true if task is a recurring instance func (t *Task) IsRecurringInstance() bool { return t.ParentUUID != nil }