package client

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"slices"

	"github.com/moby/moby/api/types/build"
	"github.com/moby/moby/api/types/container"
	"github.com/moby/moby/api/types/image"
	"github.com/moby/moby/api/types/system"
	"github.com/moby/moby/api/types/volume"
	"github.com/moby/moby/client/pkg/versions"
)

// DiskUsageOptions holds parameters for [Client.DiskUsage] operations.
type DiskUsageOptions struct {
	// Containers controls whether container disk usage should be computed.
	Containers bool

	// Images controls whether image disk usage should be computed.
	Images bool

	// BuildCache controls whether build cache disk usage should be computed.
	BuildCache bool

	// Volumes controls whether volume disk usage should be computed.
	Volumes bool

	// Verbose enables more detailed disk usage information.
	Verbose bool
}

// DiskUsageResult is the result of [Client.DiskUsage] operations.
type DiskUsageResult struct {
	// Containers holds container disk usage information.
	Containers ContainersDiskUsage

	// Images holds image disk usage information.
	Images ImagesDiskUsage

	// BuildCache holds build cache disk usage information.
	BuildCache BuildCacheDiskUsage

	// Volumes holds volume disk usage information.
	Volumes VolumesDiskUsage
}

// ContainersDiskUsage contains disk usage information for containers.
type ContainersDiskUsage struct {
	// ActiveCount is the number of active containers.
	ActiveCount int64

	// TotalCount is the total number of containers.
	TotalCount int64

	// Reclaimable is the amount of disk space that can be reclaimed.
	Reclaimable int64

	// TotalSize is the total disk space used by all containers.
	TotalSize int64

	// Items holds detailed information about each container.
	Items []container.Summary
}

// ImagesDiskUsage contains disk usage information for images.
type ImagesDiskUsage struct {
	// ActiveCount is the number of active images.
	ActiveCount int64

	// TotalCount is the total number of images.
	TotalCount int64

	// Reclaimable is the amount of disk space that can be reclaimed.
	Reclaimable int64

	// TotalSize is the total disk space used by all images.
	TotalSize int64

	// Items holds detailed information about each image.
	Items []image.Summary
}

// VolumesDiskUsage contains disk usage information for volumes.
type VolumesDiskUsage struct {
	// ActiveCount is the number of active volumes.
	ActiveCount int64

	// TotalCount is the total number of volumes.
	TotalCount int64

	// Reclaimable is the amount of disk space that can be reclaimed.
	Reclaimable int64

	// TotalSize is the total disk space used by all volumes.
	TotalSize int64

	// Items holds detailed information about each volume.
	Items []volume.Volume
}

// BuildCacheDiskUsage contains disk usage information for build cache.
type BuildCacheDiskUsage struct {
	// ActiveCount is the number of active build cache records.
	ActiveCount int64

	// TotalCount is the total number of build cache records.
	TotalCount int64

	// Reclaimable is the amount of disk space that can be reclaimed.
	Reclaimable int64

	// TotalSize is the total disk space used by all build cache records.
	TotalSize int64

	// Items holds detailed information about each build cache record.
	Items []build.CacheRecord
}

// DiskUsage requests the current data usage from the daemon.
func (cli *Client) DiskUsage(ctx context.Context, options DiskUsageOptions) (DiskUsageResult, error) {
	query := url.Values{}

	for _, t := range []struct {
		flag   bool
		sysObj system.DiskUsageObject
	}{
		{options.Containers, system.ContainerObject},
		{options.Images, system.ImageObject},
		{options.Volumes, system.VolumeObject},
		{options.BuildCache, system.BuildCacheObject},
	} {
		if t.flag {
			query.Add("type", string(t.sysObj))
		}
	}

	if options.Verbose {
		query.Set("verbose", "1")
	}

	resp, err := cli.get(ctx, "/system/df", query, nil)
	defer ensureReaderClosed(resp)
	if err != nil {
		return DiskUsageResult{}, err
	}

	if versions.LessThan(cli.version, "1.52") {
		// Generate result from a legacy response.
		var du legacyDiskUsage
		if err := json.NewDecoder(resp.Body).Decode(&du); err != nil {
			return DiskUsageResult{}, fmt.Errorf("retrieving disk usage: %v", err)
		}

		return diskUsageResultFromLegacyAPI(&du), nil
	}

	var du system.DiskUsage
	if err := json.NewDecoder(resp.Body).Decode(&du); err != nil {
		return DiskUsageResult{}, fmt.Errorf("retrieving disk usage: %v", err)
	}

	var r DiskUsageResult
	if idu := du.ImageUsage; idu != nil {
		r.Images = ImagesDiskUsage{
			ActiveCount: idu.ActiveCount,
			Reclaimable: idu.Reclaimable,
			TotalCount:  idu.TotalCount,
			TotalSize:   idu.TotalSize,
		}

		if options.Verbose {
			r.Images.Items = slices.Clone(idu.Items)
		}
	}

	if cdu := du.ContainerUsage; cdu != nil {
		r.Containers = ContainersDiskUsage{
			ActiveCount: cdu.ActiveCount,
			Reclaimable: cdu.Reclaimable,
			TotalCount:  cdu.TotalCount,
			TotalSize:   cdu.TotalSize,
		}

		if options.Verbose {
			r.Containers.Items = slices.Clone(cdu.Items)
		}
	}

	if bdu := du.BuildCacheUsage; bdu != nil {
		r.BuildCache = BuildCacheDiskUsage{
			ActiveCount: bdu.ActiveCount,
			Reclaimable: bdu.Reclaimable,
			TotalCount:  bdu.TotalCount,
			TotalSize:   bdu.TotalSize,
		}

		if options.Verbose {
			r.BuildCache.Items = slices.Clone(bdu.Items)
		}
	}

	if vdu := du.VolumeUsage; vdu != nil {
		r.Volumes = VolumesDiskUsage{
			ActiveCount: vdu.ActiveCount,
			Reclaimable: vdu.Reclaimable,
			TotalCount:  vdu.TotalCount,
			TotalSize:   vdu.TotalSize,
		}

		if options.Verbose {
			r.Volumes.Items = slices.Clone(vdu.Items)
		}
	}

	return r, nil
}

// legacyDiskUsage is the response as was used by API < v1.52.
type legacyDiskUsage struct {
	LayersSize int64               `json:"LayersSize,omitempty"`
	Images     []image.Summary     `json:"Images,omitzero"`
	Containers []container.Summary `json:"Containers,omitzero"`
	Volumes    []volume.Volume     `json:"Volumes,omitzero"`
	BuildCache []build.CacheRecord `json:"BuildCache,omitzero"`
}

func diskUsageResultFromLegacyAPI(du *legacyDiskUsage) DiskUsageResult {
	return DiskUsageResult{
		Images:     imageDiskUsageFromLegacyAPI(du),
		Containers: containerDiskUsageFromLegacyAPI(du),
		BuildCache: buildCacheDiskUsageFromLegacyAPI(du),
		Volumes:    volumeDiskUsageFromLegacyAPI(du),
	}
}

func imageDiskUsageFromLegacyAPI(du *legacyDiskUsage) ImagesDiskUsage {
	idu := ImagesDiskUsage{
		TotalSize:  du.LayersSize,
		TotalCount: int64(len(du.Images)),
		Items:      du.Images,
	}

	var used int64
	for _, i := range idu.Items {
		if i.Containers > 0 {
			idu.ActiveCount++

			if i.Size == -1 || i.SharedSize == -1 {
				continue
			}
			used += (i.Size - i.SharedSize)
		}
	}

	if idu.TotalCount > 0 {
		idu.Reclaimable = idu.TotalSize - used
	}

	return idu
}

func containerDiskUsageFromLegacyAPI(du *legacyDiskUsage) ContainersDiskUsage {
	cdu := ContainersDiskUsage{
		TotalCount: int64(len(du.Containers)),
		Items:      du.Containers,
	}

	var used int64
	for _, c := range cdu.Items {
		cdu.TotalSize += c.SizeRw
		switch c.State {
		case container.StateRunning, container.StatePaused, container.StateRestarting:
			cdu.ActiveCount++
			used += c.SizeRw
		}
	}

	cdu.Reclaimable = cdu.TotalSize - used
	return cdu
}

func buildCacheDiskUsageFromLegacyAPI(du *legacyDiskUsage) BuildCacheDiskUsage {
	bdu := BuildCacheDiskUsage{
		TotalCount: int64(len(du.BuildCache)),
		Items:      du.BuildCache,
	}

	var used int64
	for _, b := range du.BuildCache {
		if !b.Shared {
			bdu.TotalSize += b.Size
		}

		if b.InUse {
			bdu.ActiveCount++
			if !b.Shared {
				used += b.Size
			}
		}
	}

	bdu.Reclaimable = bdu.TotalSize - used
	return bdu
}

func volumeDiskUsageFromLegacyAPI(du *legacyDiskUsage) VolumesDiskUsage {
	vdu := VolumesDiskUsage{
		TotalCount: int64(len(du.Volumes)),
		Items:      du.Volumes,
	}

	var used int64
	for _, v := range vdu.Items {
		// Ignore volumes with no usage data
		if v.UsageData != nil {
			if v.UsageData.RefCount > 0 {
				vdu.ActiveCount++
				used += v.UsageData.Size
			}
			if v.UsageData.Size > 0 {
				vdu.TotalSize += v.UsageData.Size
			}
		}
	}

	vdu.Reclaimable = vdu.TotalSize - used
	return vdu
}
