// +k8s:deepcopy-gen=package

package v1

import (
	"strings"

	"golang.org/x/exp/maps"
	"golang.org/x/exp/slices"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type AppInstanceCondition string

var (
	AppInstanceConditionDefined             = "defined"
	AppInstanceConditionDefaults            = "defaults"
	AppInstanceConditionResolvedOfferings   = "resolved-offerings"
	AppInstanceConditionScheduling          = "scheduling"
	AppInstanceConditionNamespace           = "namespace"
	AppInstanceConditionParsed              = "parsed"
	AppInstanceConditionController          = "controller"
	AppInstanceConditionPulled              = "image-pull"
	AppInstanceConditionSecrets             = "secrets"
	AppInstanceConditionServices            = "services"
	AppInstanceConditionContainers          = "containers"
	AppInstanceConditionFunctions           = "functions"
	AppInstanceConditionJobs                = "jobs"
	AppInstanceConditionAcorns              = "acorns"
	AppInstanceConditionRouters             = "routers"
	AppInstanceConditionPermissions         = "permissions"
	AppInstanceConditionConsumerPermissions = "consumer-permissions"
	AppInstanceConditionReady               = "Ready"
	AppInstanceConditionVolumes             = "volumes"
	AppInstanceConditionQuota               = "quota"
)

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type AppInstanceList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []AppInstance `json:"items"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type AppInstance struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	Spec   AppInstanceSpec   `json:"spec,omitempty"`
	Status AppInstanceStatus `json:"status,omitempty"`
}

func (in *AppInstance) HasRegion(region string) bool {
	return in.Status.Defaults.Region == region || in.Spec.Region == region
}

func (in *AppInstance) GetRegion() string {
	if in.Spec.Region != "" {
		return in.Spec.Region
	}
	return in.Status.Defaults.Region
}

func (in *AppInstance) SetDefaultRegion(region string) {
	if in.Spec.Region == "" {
		if in.Status.Defaults.Region == "" {
			in.Status.Defaults.Region = region
		}
	} else {
		in.Status.Defaults.Region = ""
	}
}

func (in *AppInstance) ShortID() string {
	if len(in.UID) > 11 {
		return string(in.UID[:12])
	}
	return string(in.UID)
}

type PublishMode string

const (
	PublishModeAll     = PublishMode("all")
	PublishModeNone    = PublishMode("none")
	PublishModeDefined = PublishMode("defined")
)

type AppInstanceSpec struct {
	Region                  string           `json:"region,omitempty"`
	Labels                  []ScopedLabel    `json:"labels,omitempty"`
	Annotations             []ScopedLabel    `json:"annotations,omitempty"`
	Image                   string           `json:"image,omitempty"`
	Stop                    *bool            `json:"stop,omitempty"`
	Profiles                []string         `json:"profiles,omitempty"`
	Volumes                 []VolumeBinding  `json:"volumes,omitempty"`
	Secrets                 []SecretBinding  `json:"secrets,omitempty"`
	Environment             []NameValue      `json:"environment,omitempty"`
	PublishMode             PublishMode      `json:"publishMode,omitempty"`
	Links                   []ServiceBinding `json:"services,omitempty"`
	Publish                 []PortBinding    `json:"ports,omitempty"`
	DeployArgs              *GenericMap      `json:"deployArgs,omitempty"`
	GrantedPermissions      []Permissions    `json:"permissions,omitempty"`             // Permissions granted by the user (later mixed with other granted perms)
	ImageGrantedPermissions []Permissions    `json:"imageGrantedPermissions,omitempty"` // Permissions implicitly granted to the image
	AutoUpgrade             *bool            `json:"autoUpgrade,omitempty"`
	NotifyUpgrade           *bool            `json:"notifyUpgrade,omitempty"`
	AutoUpgradeInterval     string           `json:"autoUpgradeInterval,omitempty"`
	ComputeClasses          ComputeClassMap  `json:"computeClass,omitempty"`
	Memory                  MemoryMap        `json:"memory,omitempty"`
}

// GetGrantedPermissions returns the permissions for the app as granted by the user or granted implicitly to the image.
// Those are not necessarily equal to the permissions actually requested by the images within the app.
func (in *AppInstanceSpec) GetGrantedPermissions() []Permissions {
	return append(in.GrantedPermissions, in.ImageGrantedPermissions...)
}

func (in *AppInstance) GetStopped() bool {
	return in.Spec.Stop != nil && *in.Spec.Stop && in.DeletionTimestamp.IsZero()
}

// GetAllContainerNames returns a string slice containing the name of every container, job, and sidecar defined in Status.AppSpec.
func (in *AppInstance) GetAllContainerNames() []string {
	allContainers := append(maps.Keys(in.Status.AppSpec.Containers), maps.Keys(in.Status.AppSpec.Jobs)...)
	for _, container := range in.Status.AppSpec.Containers {
		allContainers = append(allContainers, maps.Keys(container.Sidecars)...)
	}
	for _, job := range in.Status.AppSpec.Jobs {
		allContainers = append(allContainers, maps.Keys(job.Sidecars)...)
	}
	slices.Sort[[]string](allContainers)
	return allContainers
}

func (in *AppInstanceSpec) GetAutoUpgrade() bool {
	return in.AutoUpgrade != nil && *in.AutoUpgrade
}

func (in *AppInstanceSpec) GetNotifyUpgrade() bool {
	return in.NotifyUpgrade != nil && *in.NotifyUpgrade
}

func addProfile(profiles []string, toAdd string) []string {
	found := false
	optional := strings.HasSuffix(toAdd, "?")
	nonOptionalName := toAdd[:len(toAdd)-1]
	for _, profile := range profiles {
		if profile == toAdd || (optional && profile == nonOptionalName) {
			found = true
			break
		}
	}
	if !found {
		return append([]string{toAdd}, profiles...)
	}
	return profiles
}

func (in *AppInstanceSpec) GetProfiles(devMode bool) []string {
	profiles := in.Profiles
	if devMode {
		profiles = addProfile(profiles, "devMode?")
	}
	if in.GetAutoUpgrade() {
		profiles = addProfile(profiles, "autoUpgrade?")
	}
	return profiles
}

type ServiceBindings []ServiceBinding

type ServiceBinding struct {
	Target  string `json:"target,omitempty"`
	Service string `json:"service,omitempty"`
}

type SecretBindings []SecretBinding

type SecretBinding struct {
	Secret string `json:"secret,omitempty"`
	Target string `json:"target,omitempty"`
}

type Quantity string

type VolumeBindings []VolumeBinding

type VolumeBinding struct {
	Volume      string      `json:"volume,omitempty"`
	Target      string      `json:"target,omitempty"`
	Size        Quantity    `json:"size,omitempty"`
	AccessModes AccessModes `json:"accessModes,omitempty"`
	Class       string      `json:"class,omitempty"`
}

type AppColumns struct {
	Healthy   string `json:"healthy,omitempty" column:"name=Healthy,jsonpath=.status.columns.healthy"`
	UpToDate  string `json:"upToDate,omitempty" column:"name=Up-To-Date,jsonpath=.status.columns.upToDate"`
	Message   string `json:"message,omitempty" column:"name=Message,jsonpath=.status.columns.message"`
	Endpoints string `json:"endpoints,omitempty" column:"name=Endpoints,jsonpath=.status.columns.endpoints"`
	Created   string `json:"created,omitempty" column:"name=Created,jsonpath=.metadata.creationTimestamp"`
}

type AppInstanceStatus struct {
	EmbeddedAppStatus `json:",inline"`
	Scheduling        map[string]Scheduling `json:"scheduling,omitempty"`
}

type EmbeddedAppStatus struct {
	DevSession             *DevSessionInstanceSpec `json:"devSession,omitempty"`
	ObservedGeneration     int64                   `json:"observedGeneration,omitempty"`
	ObservedImageDigest    string                  `json:"observedImageDigest,omitempty"`
	ObservedAutoUpgrade    bool                    `json:"observedAutoUpgrade,omitempty"`
	Columns                AppColumns              `json:"columns,omitempty"`
	Ready                  bool                    `json:"ready,omitempty"`
	Namespace              string                  `json:"namespace,omitempty"`
	Staged                 AppStatusStaged         `json:"staged,omitempty"`
	AppImage               AppImage                `json:"appImage,omitempty"`
	AvailableAppImage      string                  `json:"availableAppImage,omitempty"`
	ConfirmUpgradeAppImage string                  `json:"confirmUpgradeAppImage,omitempty"`
	AppSpec                AppSpec                 `json:"appSpec,omitempty"`
	AppStatus              AppStatus               `json:"appStatus,omitempty"`
	Conditions             []Condition             `json:"conditions,omitempty"`
	Defaults               Defaults                `json:"defaults,omitempty"`
	ResolvedOfferings      ResolvedOfferings       `json:"resolvedOfferings,omitempty"`
	Summary                CommonSummary           `json:"summary,omitempty"`

	// Permissions granted to the app (only containers within, not nested Acorns/Services).
	Permissions []Permissions `json:"permissions,omitempty"`

	// Permissions required by services the app, but have not been granted to the App.
	DeniedConsumerPermissions []Permissions `json:"deniedConsumerPermissions,omitempty"`
}

func (in EmbeddedAppStatus) GetDevMode() bool {
	return in.DevSession != nil
}

type AppStatusStaged struct {
	// Staged for promotion to Status
	AppImage             AppImage      `json:"appImage,omitempty"`
	AppScopedPermissions []Permissions `json:"appScopedPermissions,omitempty"` // Permissions requested by the app (narrow scope, not including nested Acorns/Services)

	// Requirements for the AppImage to be promoted to the actual Status

	PermissionsChecked            bool          `json:"permissionsChecked,omitempty"`
	PermissionsMissing            []Permissions `json:"permissionsMissing,omitempty"`
	PermissionsObservedGeneration int64         `json:"permissionsObservedGeneration,omitempty"`
	ImagePermissionsDenied        []Permissions `json:"imagePermissionsDenied,omitempty"`
	ImageAllowed                  *bool         `json:"imageAllowed,omitempty"`
}

type Defaults struct {
	VolumeSize *resource.Quantity       `json:"volumeSize,omitempty"`
	Volumes    map[string]VolumeDefault `json:"volumes,omitempty"`
	Memory     map[string]*int64        `json:"memory,omitempty"`
	Region     string                   `json:"region,omitempty"`
}

type VolumeDefault struct {
	Class       string      `json:"class,omitempty"`
	Size        Quantity    `json:"size,omitempty"`
	AccessModes AccessModes `json:"accessModes,omitempty"`
}

type ResolvedOfferings struct {
	Volumes    map[string]VolumeResolvedOffering    `json:"volumes,omitempty"`
	VolumeSize *resource.Quantity                   `json:"volumeSize,omitempty"`
	Containers map[string]ContainerResolvedOffering `json:"containers,omitempty"`
	Region     string                               `json:"region,omitempty"`
}

type VolumeResolvedOffering struct {
	Class       string      `json:"class,omitempty"`
	Size        Quantity    `json:"size,omitempty"`
	AccessModes AccessModes `json:"accessModes,omitempty"`
}

type ContainerResolvedOffering struct {
	Class  string `json:"class,omitempty"`
	Memory *int64 `json:"memory,omitempty"`
	CPU    *int64 `json:"cpu,omitempty"`
}

type Scheduling struct {
	Requirements      corev1.ResourceRequirements `json:"requirements,omitempty"`
	Affinity          *corev1.Affinity            `json:"affinity,omitempty"`
	Tolerations       []corev1.Toleration         `json:"tolerations,omitempty"`
	PriorityClassName string                      `json:"priorityClassName,omitempty"`
	RuntimeClassName  string                      `json:"runtimeClassName,omitempty"`
}

type Endpoint struct {
	Target          string          `json:"target,omitempty"`
	TargetPort      int32           `json:"targetPort,omitempty"`
	Address         string          `json:"address,omitempty"`
	Protocol        Protocol        `json:"protocol,omitempty"`
	PublishProtocol PublishProtocol `json:"publishProtocol,omitempty"`
	Path            string          `json:"path,omitempty"`
	Pending         bool            `json:"pending,omitempty"`
}

func (in *AppInstanceStatus) Condition(name string) Condition {
	for _, cond := range in.Conditions {
		if cond.Type == name {
			return cond
		}
	}
	return Condition{}
}

func (in *AppInstance) Conditions() *[]Condition {
	return &in.Status.Conditions
}
