// Package diskutil provides the functionality necessary for interacting with macOS's diskutil CLI.
package diskutil

//go:generate mockgen -source=diskutil.go -destination=mocks/mock_diskutil.go

import (
	"errors"
	"fmt"
	"strings"

	"github.com/aws/ec2-macos-utils/internal/diskutil/types"
	"github.com/aws/ec2-macos-utils/internal/system"

	"github.com/Masterminds/semver"
)

const (
	// minimumGrowFreeSpace defines the minimum amount of free space (in bytes) required to attempt running
	// diskutil's resize command.
	minimumGrowFreeSpace = 1000000
)

// ErrReadOnly identifies errors due to dry-run not being able to continue without mutating changes.
var ErrReadOnly = errors.New("read-only mode")

// FreeSpaceError defines an error to distinguish when there's not enough space to grow the specified container.
type FreeSpaceError struct {
	freeSpaceBytes uint64
}

func (e FreeSpaceError) Error() string {
	return fmt.Sprintf("%d bytes available", e.freeSpaceBytes)
}

// DiskUtil outlines the functionality necessary for wrapping macOS's diskutil tool.
type DiskUtil interface {
	// APFS outlines the functionality necessary for wrapping diskutil's "apfs" verb.
	APFS
	// Info fetches raw disk information for the specified device identifier.
	Info(id string) (*types.DiskInfo, error)
	// List fetches all disk and partition information for the system.
	// This output will be filtered based on the args provided.
	List(args []string) (*types.SystemPartitions, error)
	// RepairDisk attempts to repair the disk for the specified device identifier.
	// This process requires root access.
	RepairDisk(id string) (string, error)
}

// APFS outlines the functionality necessary for wrapping diskutil's "apfs" verb.
type APFS interface {
	// ResizeContainer attempts to grow the APFS container with the given device identifier
	// to the specified size. If the given size is 0, ResizeContainer will attempt to grow
	// the disk to its maximum size.
	ResizeContainer(id string, size string) (string, error)
}

// readonlyWrapper provides a typed implementation for DiskUtil that substitutes mutating
// methods with dryrun alternatives.
type readonlyWrapper struct {
	// impl is the DiskUtil implementation that should have mutating methods substituted for dryrun methods.
	impl DiskUtil
}

func (r readonlyWrapper) ResizeContainer(id string, size string) (string, error) {
	return "", fmt.Errorf("skip resize container: %w", ErrReadOnly)
}

func (r readonlyWrapper) Info(id string) (*types.DiskInfo, error) {
	return r.impl.Info(id)
}

func (r readonlyWrapper) List(args []string) (*types.SystemPartitions, error) {
	return r.impl.List(args)
}

func (r readonlyWrapper) RepairDisk(id string) (string, error) {
	return "", fmt.Errorf("skip repair disk: %w", ErrReadOnly)
}

// Type assertion to ensure readonlyWrapper implements the DiskUtil interface.
var _ DiskUtil = (*readonlyWrapper)(nil)

// Dryrun takes a DiskUtil implementation and wraps the mutating methods with dryrun alternatives.
func Dryrun(impl DiskUtil) *readonlyWrapper {
	return &readonlyWrapper{impl}
}

// ForProduct creates a new diskutil controller for the given product.
func ForProduct(p *system.Product) (DiskUtil, error) {
	switch p.Release {
	case system.Mojave:
		return newMojave(p.Version)
	case system.Catalina:
		return newCatalina(p.Version)
	case system.BigSur:
		return newBigSur(p.Version)
	case system.Monterey:
		return newMonterey(p.Version)
	case system.Ventura:
		return newVentura(p.Version)
	default:
		return nil, errors.New("unknown release")
	}
}

// newMojave configures the DiskUtil for the specified Mojave version.
func newMojave(version semver.Version) (*diskutilMojave, error) {
	du := &diskutilMojave{
		embeddedDiskutil: &DiskUtilityCmd{},
		dec:              &PlistDecoder{},
	}

	return du, nil
}

// newCatalina configures the DiskUtil for the specified Catalina version.
func newCatalina(version semver.Version) (*diskutilCatalina, error) {
	du := &diskutilCatalina{
		embeddedDiskutil: &DiskUtilityCmd{},
		dec:              &PlistDecoder{},
	}

	return du, nil
}

// newBigSur configures the DiskUtil for the specified Big Sur version.
func newBigSur(version semver.Version) (*diskutilBigSur, error) {
	du := &diskutilBigSur{
		embeddedDiskutil: &DiskUtilityCmd{},
		dec:              &PlistDecoder{},
	}

	return du, nil
}

// newMonterey configures the DiskUtil for the specified Monterey version.
func newMonterey(version semver.Version) (*diskutilMonterey, error) {
	du := &diskutilMonterey{
		embeddedDiskutil: &DiskUtilityCmd{},
		dec:              &PlistDecoder{},
	}

	return du, nil
}

// newVentura configures the DiskUtil for the specified Ventura version.
func newVentura(version semver.Version) (*diskutilMonterey, error) {
	du := &diskutilMonterey{
		embeddedDiskutil: &DiskUtilityCmd{},
		dec:              &PlistDecoder{},
	}

	return du, nil
}

// embeddedDiskutil is a private interface used to embed UtilImpl into implementation-specific structs.
type embeddedDiskutil interface {
	UtilImpl
}

// diskutilMojave wraps all the functionality necessary for interacting with macOS's diskutil on Mojave. The
// major difference is that the raw plist data emitted by macOS's diskutil CLI doesn't include the physical store data.
// This requires a separate fetch to find the specific physical store information for the disk(s).
type diskutilMojave struct {
	// embeddedDiskutil provides the diskutil implementation to prevent manual wiring between UtilImpl and DiskUtil.
	embeddedDiskutil

	// dec is the Decoder used to decode the raw output from UtilImpl into usable structs.
	dec Decoder
}

// List utilizes the UtilImpl.List method to fetch the raw list output from diskutil and returns the decoded
// output in a SystemPartitions struct. List also attempts to update each APFS Volume's physical store via a separate
// fetch method since the version of diskutil on Mojave doesn't provide that information in its List verb.
//
// It is possible for List to fail when updating the physical stores, but it will still return the original data
// that was decoded into the SystemPartitions struct.
func (d *diskutilMojave) List(args []string) (*types.SystemPartitions, error) {
	partitions, err := list(d.embeddedDiskutil, d.dec, args)
	if err != nil {
		return nil, err
	}

	err = updatePhysicalStores(partitions)
	if err != nil {
		return partitions, err
	}

	return partitions, nil
}

// Info utilizes the UtilImpl.Info method to fetch the raw disk output from diskutil and returns the decoded
// output in a DiskInfo struct. Info also attempts to update each APFS Volume's physical store via a separate
// fetch method since the version of diskutil on Mojave doesn't provide that information in its Info verb.
//
// It is possible for Info to fail when updating the physical stores, but it will still return the original data
// that was decoded into the DiskInfo struct.
func (d *diskutilMojave) Info(id string) (*types.DiskInfo, error) {
	disk, err := info(d.embeddedDiskutil, d.dec, id)
	if err != nil {
		return nil, err
	}

	err = updatePhysicalStore(disk)
	if err != nil {
		return disk, err
	}

	return disk, nil
}

// diskutilCatalina wraps all the functionality necessary for interacting with macOS's diskutil in GoLang.
type diskutilCatalina struct {
	// embeddedDiskutil provides the diskutil implementation to prevent manual wiring between UtilImpl and DiskUtil.
	embeddedDiskutil

	// dec is the Decoder used to decode the raw output from UtilImpl into usable structs.
	dec Decoder
}

// List utilizes the UtilImpl.List method to fetch the raw list output from diskutil and returns the decoded
// output in a SystemPartitions struct.
func (d *diskutilCatalina) List(args []string) (*types.SystemPartitions, error) {
	return list(d.embeddedDiskutil, d.dec, args)
}

// Info utilizes the UtilImpl.Info method to fetch the raw disk output from diskutil and returns the decoded
// output in a DiskInfo struct.
func (d *diskutilCatalina) Info(id string) (*types.DiskInfo, error) {
	return info(d.embeddedDiskutil, d.dec, id)
}

// diskutilBigSur wraps all the functionality necessary for interacting with macOS's diskutil in GoLang.
type diskutilBigSur struct {
	// embeddedDiskutil provides the diskutil implementation to prevent manual wiring between UtilImpl and DiskUtil.
	embeddedDiskutil

	// dec is the Decoder used to decode the raw output from UtilImpl into usable structs.
	dec Decoder
}

// List utilizes the UtilImpl.List method to fetch the raw list output from diskutil and returns the decoded
// output in a SystemPartitions struct.
func (d *diskutilBigSur) List(args []string) (*types.SystemPartitions, error) {
	return list(d.embeddedDiskutil, d.dec, args)
}

// Info utilizes the UtilImpl.Info method to fetch the raw disk output from diskutil and returns the decoded
// output in a DiskInfo struct.
func (d *diskutilBigSur) Info(id string) (*types.DiskInfo, error) {
	return info(d.embeddedDiskutil, d.dec, id)
}

// diskutilMonterey wraps all the functionality necessary for interacting with macOS's diskutil in GoLang.
type diskutilMonterey struct {
	// embeddedDiskutil provides the diskutil implementation to prevent manual wiring between UtilImpl and DiskUtil.
	embeddedDiskutil

	// dec is the Decoder used to decode the raw output from UtilImpl into usable structs.
	dec Decoder
}

// List utilizes the UtilImpl.List method to fetch the raw list output from diskutil and returns the decoded
// output in a SystemPartitions struct.
func (d *diskutilMonterey) List(args []string) (*types.SystemPartitions, error) {
	return list(d.embeddedDiskutil, d.dec, args)
}

// Info utilizes the UtilImpl.Info method to fetch the raw disk output from diskutil and returns the decoded
// output in a DiskInfo struct.
func (d *diskutilMonterey) Info(id string) (*types.DiskInfo, error) {
	return info(d.embeddedDiskutil, d.dec, id)
}

// diskutilVentura wraps all the functionality necessary for interacting with macOS's diskutil in GoLang.
type diskutilVentura struct {
	// embeddedDiskutil provides the diskutil implementation to prevent manual wiring between UtilImpl and DiskUtil.
	embeddedDiskutil

	// dec is the Decoder used to decode the raw output from UtilImpl into usable structs.
	dec Decoder
}

// List utilizes the UtilImpl.List method to fetch the raw list output from diskutil and returns the decoded
// output in a SystemPartitions struct.
func (d *diskutilVentura) List(args []string) (*types.SystemPartitions, error) {
	return list(d.embeddedDiskutil, d.dec, args)
}

// Info utilizes the UtilImpl.Info method to fetch the raw disk output from diskutil and returns the decoded
// output in a DiskInfo struct.
func (d *diskutilVentura) Info(id string) (*types.DiskInfo, error) {
	return info(d.embeddedDiskutil, d.dec, id)
}

// info is a wrapper that fetches the raw diskutil info data and decodes it into a usable types.DiskInfo struct.
func info(util UtilImpl, decoder Decoder, id string) (*types.DiskInfo, error) {
	// Fetch the raw disk information from the util
	rawDisk, err := util.Info(id)
	if err != nil {
		return nil, err
	}

	// Create a reader for the raw data
	reader := strings.NewReader(rawDisk)

	// Decode the raw data into a more usable DiskInfo struct
	disk, err := decoder.DecodeDiskInfo(reader)
	if err != nil {
		return nil, err
	}

	return disk, nil
}

// list is a wrapper that fetches the raw diskutil list data and decodes it into a usable types.SystemPartitions struct.
func list(util UtilImpl, decoder Decoder, args []string) (*types.SystemPartitions, error) {
	// Fetch the raw list information from the util
	rawPartitions, err := util.List(args)
	if err != nil {
		return nil, err
	}

	// Create a reader for the raw data
	reader := strings.NewReader(rawPartitions)

	// Decode the raw data into a more usable SystemPartitions struct
	partitions, err := decoder.DecodeSystemPartitions(reader)
	if err != nil {
		return nil, err
	}

	return partitions, nil
}