// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package block

import (
	"fmt"
	"os"
	"time"

	"github.com/cosi-project/runtime/pkg/resource"
	"github.com/cosi-project/runtime/pkg/resource/meta"
	"github.com/cosi-project/runtime/pkg/resource/protobuf"
	"github.com/cosi-project/runtime/pkg/resource/typed"

	"github.com/siderolabs/talos/pkg/machinery/cel"
	"github.com/siderolabs/talos/pkg/machinery/proto"
	"github.com/siderolabs/talos/pkg/machinery/yamlutils"
)

// VolumeConfigType is type of VolumeConfig resource.
const VolumeConfigType = resource.Type("VolumeConfigs.block.talos.dev")

// VolumeConfig resource contains configuration for machine volumes.
type VolumeConfig = typed.Resource[VolumeConfigSpec, VolumeConfigExtension]

// VolumeConfigSpec is the spec for VolumeConfig resource.
//
//gotagsrewrite:gen
type VolumeConfigSpec struct {
	// Parent volume ID, if set no operations on the volume continue until the parent volume is ready.
	ParentID string `yaml:"parentId,omitempty" protobuf:"1"`

	// Volume type.
	Type VolumeType `yaml:"type" protobuf:"2"`

	// Provisioning configuration (how to provision a volume).
	Provisioning ProvisioningSpec `yaml:"provisioning" protobuf:"3"`

	// Encryption configuration (how to encrypt a volume).
	Encryption EncryptionSpec `yaml:"encryption,omitempty" protobuf:"6"`

	// How to find a volume.
	Locator LocatorSpec `yaml:"locator" protobuf:"4"`

	// Mount options for the volume.
	Mount MountSpec `yaml:"mount,omitempty" protobuf:"5"`

	// Symlink options for the volume.
	Symlink SymlinkProvisioningSpec `yaml:"symlink,omitempty" protobuf:"7"`

	// TrimEnabled indicates whether the volume should be trimmed (fstrim) on a schedule.
	TrimEnabled bool `yaml:"trimEnabled,omitempty" protobuf:"8"`
	// TrimInterval is the resolved interval at which the volume should be trimmed.
	TrimInterval time.Duration `yaml:"trimInterval,omitempty" protobuf:"9"`
}

// Wave constants.
const (
	WaveSystemDisk      = -1
	WaveUserVolumes     = 0
	WaveLegacyUserDisks = 1000000 // legacy user disks rely on specific order of provisioning
)

// ProvisioningSpec is the spec for volume provisioning.
//
//gotagsrewrite:gen
type ProvisioningSpec struct {
	// Provisioning wave for the volume.
	//
	// Waves are processed sequentially - the volumes in the wave are only provisioned after the previous wave is done.
	Wave int `yaml:"wave,omitempty" protobuf:"3"`

	// DiskSelector selects a disk for the volume.
	DiskSelector DiskSelector `yaml:"diskSelector,omitempty" protobuf:"1"`

	// PartitionSpec describes how to provision the volume (partition type).
	PartitionSpec PartitionSpec `yaml:"partitionSpec,omitempty" protobuf:"2"`

	// FilesystemSpec describes how to provision the volume (filesystem type).
	FilesystemSpec FilesystemSpec `yaml:"filesystemSpec,omitempty" protobuf:"4"`
}

// DiskSelector selects a disk for the volume.
//
//gotagsrewrite:gen
type DiskSelector struct {
	Match    cel.Expression `yaml:"match,omitempty" protobuf:"1"`
	External string         `yaml:"external,omitempty" protobuf:"2"`
}

// PartitionSpec is the spec for volume partitioning.
//
//gotagsrewrite:gen
type PartitionSpec struct {
	// Partition minimum size in bytes.
	MinSize uint64 `yaml:"minSize" protobuf:"1"`

	// Partition maximum size in bytes, if not set, grows to the maximum size.
	MaxSize uint64 `yaml:"maxSize,omitempty" protobuf:"2"`

	// Partition maximum size (relative), if not set, grows to the maximum size.
	RelativeMaxSize uint64 `yaml:"relativeMaxSize" protobuf:"6"`

	// NegativeMaxSize indicates that MaxSize or RelativeMaxSize represents space to be left free on the device rather than space to consume.
	NegativeMaxSize bool `yaml:"negativeMaxSize" protobuf:"7"`

	// Grow the partition automatically to the maximum size.
	Grow bool `yaml:"grow" protobuf:"3"`

	// Label for the partition.
	Label string `yaml:"label,omitempty" protobuf:"4"`

	// Partition type UUID.
	TypeUUID string `yaml:"typeUUID,omitempty" protobuf:"5"`
}

// ResolveMaxSize resolves the maximum size of the partition.
// Returns the maximum size of the partition and an error if the partition size cannot be resolved.
func (ps *PartitionSpec) ResolveMaxSize(available uint64) (uint64, error) {
	var size uint64

	if ps.RelativeMaxSize != 0 {
		size = available * ps.RelativeMaxSize / 100
	} else {
		size = ps.MaxSize
	}

	if ps.NegativeMaxSize {
		if size > available {
			return 0, fmt.Errorf("partition size cannot be negative")
		}

		return available - size, nil
	}

	return size, nil
}

// LocatorSpec is the spec for volume locator.
//
//gotagsrewrite:gen
type LocatorSpec struct {
	// Match is a volume locator match expression.
	Match cel.Expression `yaml:"match,omitempty" protobuf:"1"`
	// DiskMatch is a disk locator match expression.
	DiskMatch cel.Expression `yaml:"diskMatch,omitempty" protobuf:"2"`
}

// FilesystemSpec is the spec for volume filesystem.
//
//gotagsrewrite:gen
type FilesystemSpec struct {
	// Filesystem type.
	Type FilesystemType `yaml:"type" protobuf:"1"`
	// Filesystem label.
	Label string `yaml:"label,omitempty" protobuf:"2"`
}

// EncryptionSpec is the spec for volume encryption.
//
//gotagsrewrite:gen
type EncryptionSpec struct {
	Provider      EncryptionProviderType `yaml:"provider" protobuf:"1"`
	Keys          []EncryptionKey        `yaml:"keys" protobuf:"2"`
	Cipher        string                 `yaml:"cipher,omitempty" protobuf:"3"`
	KeySize       uint                   `yaml:"keySize,omitempty" protobuf:"4"`
	BlockSize     uint64                 `yaml:"blockSize,omitempty" protobuf:"5"`
	PerfOptions   []string               `yaml:"perfOptions,omitempty" protobuf:"6"`
	AllowDiscards bool                   `yaml:"allowDiscards,omitempty" protobuf:"7"`
}

// EncryptionKey is the spec for volume encryption key.
//
//gotagsrewrite:gen
type EncryptionKey struct {
	Slot        int               `yaml:"slot" protobuf:"1"`
	Type        EncryptionKeyType `yaml:"type" protobuf:"2"`
	LockToSTATE bool              `yaml:"lockToState,omitempty" protobuf:"6"`

	// Only for Type == "static":
	StaticPassphrase yamlutils.StringBytes `yaml:"staticPassphrase,omitempty" protobuf:"3"`

	// Only for Type == "kms":
	KMSEndpoint string `yaml:"kmsEndpoint,omitempty" protobuf:"4"`

	// Only for Type == "tpm":
	TPMCheckSecurebootStatusOnEnroll bool `yaml:"tpmCheckSecurebootStatusOnEnroll,omitempty" protobuf:"5"`
	// Only for Type == "tpm":
	TPMPCRs []int `yaml:"pcrs,omitempty" protobuf:"7"`
	// Only for Type == "tpm":
	TPMPubKeyPCRs []int `yaml:"pubKeyPcrs,omitempty" protobuf:"8"`
}

// ParameterSpec is a mount parameter.
//
//gotagsrewrite:gen
type ParameterSpec struct {
	// Type of the parameter.
	Type FSParameterType `yaml:"type" protobuf:"1"`
	// Name of the parameter.
	Name string `yaml:"name" protobuf:"2"`
	// String value of the parameter.
	String *string `yaml:"string,omitempty" protobuf:"3"`
	// Binary value of the parameter.
	Binary []byte `yaml:"binary,omitempty" protobuf:"5"`
}

// NewBooleanParameter creates a new boolean parameter.
func NewBooleanParameter(name string) ParameterSpec {
	return ParameterSpec{
		Type: FSParameterTypeBooleanValue,
		Name: name,
	}
}

// NewBinaryParameter creates a new binary parameter.
func NewBinaryParameter(name string, value []byte) ParameterSpec {
	return ParameterSpec{
		Type:   FSParameterTypeBinaryValue,
		Name:   name,
		Binary: value,
	}
}

// NewStringParameter creates a new string parameter.
func NewStringParameter(name, value string) ParameterSpec {
	return ParameterSpec{
		Type:   FSParameterTypeStringValue,
		Name:   name,
		String: new(value),
	}
}

// MountSpec is the spec for volume mount.
//
//gotagsrewrite:gen
type MountSpec struct {
	// Mount path for the volume.
	TargetPath string `yaml:"targetPath" protobuf:"1"`
	// SELinux label for the volume.
	SelinuxLabel string `yaml:"selinuxLabel" protobuf:"2"`
	// Enable project quota (xfs) for the volume.
	ProjectQuotaSupport bool `yaml:"projectQuotaSupport" protobuf:"3"`
	// Parent mount request ID.
	ParentID string `yaml:"parentId,omitempty" protobuf:"4"`
	// FileMode is the file mode for the mount target.
	FileMode os.FileMode `yaml:"fileMode,omitempty" protobuf:"5"`
	// UID is the user ID for the mount target.
	UID int `yaml:"uid,omitempty" protobuf:"6"`
	// GID is the group ID for the mount target.
	GID int `yaml:"gid,omitempty" protobuf:"7"`
	// RecursiveRelabel is the recursive relabel/chown flag for the mount target.
	RecursiveRelabel bool `yaml:"recursiveRelabel,omitempty" protobuf:"8"`
	// BindTarget is an optional path on the host to bind-mount the volume onto.
	BindTarget *string `yaml:"bindTarget,omitempty" protobuf:"9"`
	// Parameters are additional filesystem mount options used when mounting the volume.
	Parameters []ParameterSpec `yaml:"parameters,omitempty" protobuf:"10"`
	// Secure applies MOUNT_ATTR_NOSUID\|NODEV\|NOEXEC to the mount. Set for
	// config-only mounts; leave false for mounts hosting executables.
	Secure bool `yaml:"secure,omitempty" protobuf:"11"`
}

// SymlinkProvisioningSpec is the spec for volume symlink.
//
//gotagsrewrite:gen
type SymlinkProvisioningSpec struct {
	// Symlink target path for the volume.
	SymlinkTargetPath string `yaml:"symlinkTargetPath" protobuf:"1"`
	// Force symlink creation.
	Force bool `yaml:"force" protobuf:"2"`
}

// NewVolumeConfig initializes a BlockVolumeConfig resource.
func NewVolumeConfig(namespace resource.Namespace, id resource.ID) *VolumeConfig {
	return typed.NewResource[VolumeConfigSpec, VolumeConfigExtension](
		resource.NewMetadata(namespace, VolumeConfigType, id, resource.VersionUndefined),
		VolumeConfigSpec{},
	)
}

// VolumeConfigExtension is auxiliary resource data for BlockVolumeConfig.
type VolumeConfigExtension struct{}

// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (VolumeConfigExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
	return meta.ResourceDefinitionSpec{
		Type:             VolumeConfigType,
		Aliases:          []resource.Type{},
		DefaultNamespace: NamespaceName,
		PrintColumns:     []meta.PrintColumn{},
		Sensitivity:      meta.Sensitive,
	}
}

func init() {
	proto.RegisterDefaultTypes()

	err := protobuf.RegisterDynamic(VolumeConfigType, &VolumeConfig{})
	if err != nil {
		panic(err)
	}
}
