// Package storage inspects local usage and applies approved, revalidated cleanup plans.
package storage

import (
	"albedo/cli/internal/daemon"
	"context"
	"errors"
	"fmt"
	"os"
	"regexp"
	"slices"
	"time"
)

// Service keeps offline inspection separate from daemon-backed session deletion.
type Service struct {
	OnlineReport   func(context.Context) (*daemon.StorageReport, error)
	Now            func() time.Time
	Running        func() (bool, error)
	DeleteSessions func(context.Context, []string) error
	Home           string
}
type CleanupOptions struct {
	Sessions                         []string
	All, OldKernels, Backups, Vacuum bool
}

// CleanupPlan exposes a copy of the preview; execution uses its private approved values.
type CleanupPlan struct {
	files   map[string]os.FileInfo
	home    string
	options CleanupOptions
	preview daemon.StorageReport
}

func (p CleanupPlan) Preview() daemon.StorageReport {
	result := p.preview
	result.DB.Sessions = slices.Clone(result.DB.Sessions)
	result.OldKernels = slices.Clone(result.OldKernels)
	result.OldBackups = slices.Clone(result.OldBackups)
	return result
}
func (p CleanupPlan) Options() CleanupOptions {
	options := p.options
	options.Sessions = slices.Clone(options.Sessions)
	return options
}

type CleanupResult struct {
	VacuumSkipped bool
	Vacuumed      bool
	Before, After int64
}

var storageID = regexp.MustCompile(`^[0-9a-f]{32}$`)

func (s *Service) PlanCleanup(ctx context.Context, options CleanupOptions) (CleanupPlan, error) {
	var plan CleanupPlan
	options.Sessions = slices.Clone(options.Sessions)
	for i, id := range options.Sessions {
		if !storageID.MatchString(id) {
			return plan, errors.New("--session needs a full session ID; find it with albedo sessions")
		}
		if slices.Contains(options.Sessions[:i], id) {
			return plan, fmt.Errorf("session %s was selected more than once", id)
		}
	}
	if options.All {
		if len(options.Sessions) > 0 || options.OldKernels || options.Backups || options.Vacuum {
			return plan, errors.New("--all cannot be combined with other cleanup options; use --all on its own or choose individual options")
		}
		options.OldKernels, options.Backups, options.Vacuum = true, true, true
	}
	if len(options.Sessions) == 0 && !options.OldKernels && !options.Backups && !options.Vacuum {
		return plan, errors.New("choose what to clean up: --all, --session, --old-kernels, --backups or --vacuum")
	}
	if len(options.Sessions) > 0 && (options.OldKernels || options.Backups || options.Vacuum) {
		return plan, errors.New("delete sessions and clean up local files in separate commands; session deletion needs Albedo running, file cleanup needs it stopped")
	}
	var preview daemon.StorageReport
	var err error
	if len(options.Sessions) > 0 {
		if s.OnlineReport == nil {
			return plan, errors.New("session deletion needs a running daemon; start Albedo, then try again")
		}
		online, reportErr := s.OnlineReport(ctx)
		if reportErr != nil {
			return plan, reportErr
		}
		if online == nil {
			return plan, errors.New("session deletion needs a running daemon; start Albedo, then try again")
		}
		preview = *online
	} else {
		preview, err = s.OfflineReport(ctx)
	}
	if err != nil {
		return plan, err
	}
	if len(options.Sessions) == 0 {
		if options.OldKernels && preview.Database == 0 {
			return plan, errors.New("cannot identify unused Python state files without the SQLite database; no files have been removed")
		}
		running, err := s.Running()
		if err != nil {
			return plan, err
		}
		if running {
			return plan, errors.New("cleanup needs Albedo stopped; no files have been removed; when its work has finished, run albedo daemon --stop, then try again")
		}
	} else {
		for _, id := range options.Sessions {
			if !slices.ContainsFunc(preview.DB.Sessions, func(session daemon.StorageSession) bool { return session.ID == id }) {
				return plan, fmt.Errorf("no session has the ID %s; use the full ID from albedo sessions", id)
			}
		}
	}
	plan = CleanupPlan{preview: preview, options: options, home: s.Home, files: make(map[string]os.FileInfo)}
	if len(options.Sessions) > 0 {
		return plan, nil
	}
	for _, file := range append(slices.Clone(preview.OldKernels), preview.OldBackups...) {
		if err := ctx.Err(); err != nil {
			return CleanupPlan{}, err
		}
		info, err := os.Lstat(file.Path)
		if err != nil {
			return CleanupPlan{}, err
		}
		plan.files[file.Path] = info
	}
	return plan, nil
}
func (s *Service) ApplyCleanup(ctx context.Context, plan CleanupPlan) (CleanupResult, error) {
	var result CleanupResult
	if plan.home != s.Home || plan.files == nil {
		return result, errors.New("cleanup plan does not belong to this storage directory")
	}
	options, preview := plan.options, plan.preview
	if len(options.Sessions) > 0 {
		return result, s.DeleteSessions(ctx, options.Sessions)
	}
	running, err := s.Running()
	if err != nil {
		return result, err
	}
	if running {
		return result, errors.New("daemon started while you were confirming cleanup; no files have been removed; stop Albedo and try again")
	}
	var files []daemon.StorageFile
	if options.OldKernels {
		files = append(files, preview.OldKernels...)
	}
	if options.Backups {
		files = append(files, preview.OldBackups...)
	}
	helper, err := startMaintenance(ctx, s.Home, files)
	if err != nil {
		return result, err
	}
	defer helper.close()
	fresh, err := s.OfflineReport(ctx)
	if err != nil {
		if ctx.Err() != nil {
			return result, ctx.Err()
		}
		return result, err
	}
	if options.OldKernels && fresh.Database == 0 {
		return result, errors.New("database disappeared after the preview; no Python state files have been removed; restore the database and retry cleanup")
	}
	if options.OldKernels && !slices.Equal(preview.OldKernels, fresh.OldKernels) {
		return result, errors.New("unused Python state files changed after the preview; no files have been removed; run cleanup again to review the new list")
	}
	if options.Backups && !slices.Equal(preview.OldBackups, fresh.OldBackups) {
		return result, errors.New("backups changed after the preview; no backups have been removed; run cleanup again to review the new list")
	}
	// Recheck all approved candidates before deleting any, including replacements of equal size.
	for _, file := range files {
		if err := ctx.Err(); err != nil {
			return result, err
		}
		info, err := os.Lstat(file.Path)
		if err != nil {
			return result, err
		}
		approved := plan.files[file.Path]
		if !info.Mode().IsRegular() || !os.SameFile(approved, info) || info.Size() != approved.Size() || !info.ModTime().Equal(approved.ModTime()) {
			return result, fmt.Errorf("file changed after the preview; cleanup stopped: %s", file.Path)
		}
	}
	if err := ctx.Err(); err != nil {
		return result, err
	}
	return helper.apply(ctx, options.Vacuum, fresh)
}
