package docker

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/moby/moby/client"
	"golang.org/x/sync/errgroup"
)

// SystemStats is a host-level snapshot combining daemon capacity
// (NCPU, memory total) with container counts and disk usage broken down
// by category. Workload CPU/memory utilization is aggregated from
// per-container samples by the stats collector, not here.
type SystemStats struct {
	Timestamp time.Time `json:"timestamp"`

	// Capacity from Docker daemon.
	NCPU        int   `json:"ncpu"`
	MemoryTotal int64 `json:"memory_total"`

	// Container/image counts.
	Containers int `json:"containers"`
	Running    int `json:"running"`
	Paused     int `json:"paused"`
	Stopped    int `json:"stopped"`
	Images     int `json:"images"`

	// Disk usage by category (bytes).
	DiskImagesBytes     int64 `json:"disk_images_bytes"`
	DiskContainersBytes int64 `json:"disk_containers_bytes"`
	DiskVolumesBytes    int64 `json:"disk_volumes_bytes"`
	DiskBuildCacheBytes int64 `json:"disk_build_cache_bytes"`

	// Reclaimable disk space by category (bytes).
	DiskImagesReclaimable     int64 `json:"disk_images_reclaimable"`
	DiskContainersReclaimable int64 `json:"disk_containers_reclaimable"`
	DiskVolumesReclaimable    int64 `json:"disk_volumes_reclaimable"`
	DiskBuildCacheReclaimable int64 `json:"disk_build_cache_reclaimable"`

	// DiskTotalBytes is the sum of the category totals.
	DiskTotalBytes int64 `json:"disk_total_bytes"`
}

// GetSystemStats returns a one-shot host-level snapshot. Info and DiskUsage
// are issued in parallel because DiskUsage walks every layer/volume and is
// often the slowest call on a busy host (1-3 s); Info typically completes in
// ~10 ms. Disk usage failures do not fail the whole call — the result
// degrades gracefully with zero disk fields and a warning log.
func (c *Client) GetSystemStats(ctx context.Context) (SystemStats, error) {
	stats := SystemStats{Timestamp: time.Now().UTC()}

	g, gctx := errgroup.WithContext(ctx)

	g.Go(func() error {
		info, err := c.Info(gctx)
		if err != nil {
			return fmt.Errorf("system stats info: %w", err)
		}
		stats.NCPU = info.NCPU
		stats.MemoryTotal = info.MemoryTotal
		stats.Containers = info.Containers
		stats.Running = info.Running
		stats.Paused = info.Paused
		stats.Stopped = info.Stopped
		stats.Images = info.Images
		return nil
	})

	var du *client.DiskUsageResult
	g.Go(func() error {
		usage, err := c.api.DiskUsage(gctx, client.DiskUsageOptions{
			Containers: true,
			Images:     true,
			Volumes:    true,
			BuildCache: true,
		})
		if err != nil {
			// Disk usage is best-effort; swallow but log so the dashboard
			// shows zeroed disk fields rather than failing entirely.
			slog.Warn("system stats: disk usage failed", "error", err)
			return nil
		}
		du = &usage
		return nil
	})

	if err := g.Wait(); err != nil {
		return SystemStats{}, err
	}

	if du != nil {
		stats.DiskImagesBytes = du.Images.TotalSize
		stats.DiskContainersBytes = du.Containers.TotalSize
		stats.DiskVolumesBytes = du.Volumes.TotalSize
		stats.DiskBuildCacheBytes = du.BuildCache.TotalSize
		stats.DiskImagesReclaimable = du.Images.Reclaimable
		stats.DiskContainersReclaimable = du.Containers.Reclaimable
		stats.DiskVolumesReclaimable = du.Volumes.Reclaimable
		stats.DiskBuildCacheReclaimable = du.BuildCache.Reclaimable
		stats.DiskTotalBytes = stats.DiskImagesBytes +
			stats.DiskContainersBytes +
			stats.DiskVolumesBytes +
			stats.DiskBuildCacheBytes
	}

	return stats, nil
}

// BuildCachePruneResult reports the outcome of a build-cache prune.
type BuildCachePruneResult struct {
	CachesDeleted  int   `json:"caches_deleted"`  // number of cache records removed
	SpaceReclaimed int64 `json:"space_reclaimed"` // bytes reclaimed
}

// PruneBuildCache deletes unused Docker build-cache records and returns the
// number of records removed and bytes reclaimed. Docker's build-cache API is
// prune-by-filter only — there is no surgical per-record eviction — so this
// is the daemon-wide "prune unused" operation.
//
// When all is false (the default), only build cache not currently in use is
// removed, so an app's next rebuild still hits its warm cache. When all is
// true, every build-cache record is removed regardless of use, forcing a cold
// rebuild for every app.
func (c *Client) PruneBuildCache(ctx context.Context, all bool) (BuildCachePruneResult, error) {
	res, err := c.api.BuildCachePrune(ctx, client.BuildCachePruneOptions{All: all})
	if err != nil {
		return BuildCachePruneResult{}, fmt.Errorf("prune build cache: %w", err)
	}
	return BuildCachePruneResult{
		CachesDeleted:  len(res.Report.CachesDeleted),
		SpaceReclaimed: int64(res.Report.SpaceReclaimed),
	}, nil
}
