/*
Copyright 2025 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

import (
	"strings"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	gotkmeta "github.com/fluxcd/pkg/apis/meta"
)

const (
	ArtifactGeneratorKind            = "ArtifactGenerator"
	Finalizer                        = "source.extensions.fluxcd.io/finalizer"
	ArtifactGeneratorLabel           = "source.extensions.fluxcd.io/generator"
	ArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision"
	ReconcileAnnotation              = "source.extensions.fluxcd.io/reconcile"
	ReconciliationDisabledReason     = "ReconciliationDisabled"
	AccessDeniedReason               = "AccessDenied"
	ValidationFailedReason           = "ValidationFailed"
	SourceFetchFailedReason          = "SourceFetchFailed"
	OverwriteStrategy                = "Overwrite"
	MergeStrategy                    = "Merge"
	ExtractStrategy                  = "Extract"
	EnabledValue                     = "enabled"
	DisabledValue                    = "disabled"
)

// CommonMetadata defines the common labels and annotations.
type CommonMetadata struct {
	// Annotations to be added to the object's metadata.
	// +optional
	Annotations map[string]string `json:"annotations,omitempty"`

	// Labels to be added to the object's metadata.
	// +optional
	Labels map[string]string `json:"labels,omitempty"`
}

// ArtifactGeneratorSpec defines the desired state of ArtifactGenerator.
// +kubebuilder:validation:XValidation:rule="has(self.pathPattern) && size(self.pathPattern) > 0 || self.artifacts.all(a, a.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'))",message="artifact names must be valid Kubernetes object names when pathPattern is not set"
type ArtifactGeneratorSpec struct {
	// CommonMetadata specifies the common labels and annotations that are
	// applied to all resources. Any existing label or annotation will be
	// overridden if its key matches a common one.
	// +optional
	CommonMetadata *CommonMetadata `json:"commonMetadata,omitempty"`

	// Sources is a list of references to the Flux source-controller
	// resources that will be used to generate the artifact.
	// +kubebuilder:validation:MinItems=1
	// +kubebuilder:validation:MaxItems=1000
	// +required
	Sources []SourceReference `json:"sources"`

	// PathPattern specifies a directory traversal pattern to match within the sources.
	// The format is "@<alias>/<pattern>". Named captures in the pattern (e.g. "{app}")
	// can be used as placeholders in OutputArtifacts fields.
	// +kubebuilder:validation:Pattern="^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)/(.*)$"
	// +kubebuilder:validation:MaxLength=1024
	// +optional
	PathPattern string `json:"pathPattern,omitempty"`

	// OutputArtifacts is a list of output artifacts to be generated.
	// +kubebuilder:validation:MinItems=1
	// +kubebuilder:validation:MaxItems=1000
	// +required
	OutputArtifacts []OutputArtifact `json:"artifacts"`
}

// SourceReference contains the reference to a Flux source-controller resource.
type SourceReference struct {
	// Alias of the source within the ArtifactGenerator context.
	// The alias must be unique per ArtifactGenerator, and must consist
	// of lower case alphanumeric characters, underscores, and hyphens.
	// It must start and end with an alphanumeric character.
	// +kubebuilder:validation:Pattern="^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$"
	// +kubebuilder:validation:MaxLength=63
	// +required
	Alias string `json:"alias"`

	// Name of the source.
	// +kubebuilder:validation:Pattern="^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
	// +kubebuilder:validation:MaxLength=253
	// +required
	Name string `json:"name"`

	// Namespace of the source.
	// If not provided, defaults to the same namespace as the ArtifactGenerator.
	// +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
	// +kubebuilder:validation:MinLength=1
	// +kubebuilder:validation:MaxLength=63
	// +optional
	Namespace string `json:"namespace,omitempty"`

	// Kind of the source.
	// +kubebuilder:validation:Enum=Bucket;GitRepository;OCIRepository;HelmChart;ExternalArtifact
	// +required
	Kind string `json:"kind"`
}

// OutputArtifact defines the desired state of an ExternalArtifact
// generated by the ArtifactGenerator.
type OutputArtifact struct {
	// Name is the name of the generated artifact.
	// When pathPattern is set, this field may use capture placeholders such as "{app}".
	// +kubebuilder:validation:MinLength=1
	// +kubebuilder:validation:MaxLength=253
	// +required
	Name string `json:"name"`

	// Revision is the revision of the generated artifact.
	// If specified, it must point to an existing source alias in the format "@<alias>".
	// If not specified, the revision is automatically set to the digest of the artifact content.
	// +kubebuilder:validation:Pattern="^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)$"
	// +kubebuilder:validation:MaxLength=64
	// +optional
	Revision string `json:"revision,omitempty"`

	// OriginRevision is used to set the 'org.opencontainers.image.revision'
	// annotation on the generated artifact metadata.
	// If specified, it must point to an existing source alias in the format "@<alias>".
	// If the referenced source has an origin revision (e.g. a Git commit SHA),
	// it will be used to set the annotation on the generated artifact.
	// If the referenced source does not have an origin revision, the field is ignored.
	// +kubebuilder:validation:Pattern="^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)$"
	// +kubebuilder:validation:MaxLength=64
	// +optional
	OriginRevision string `json:"originRevision,omitempty"`

	// Copy defines a list of copy operations to perform from the sources to the generated artifact.
	// The copy operations are performed in the order they are listed with existing files
	// being overwritten by later copy operations.
	// +kubebuilder:validation:MinItems=1
	// +required
	Copy []CopyOperation `json:"copy"`
}

type CopyOperation struct {
	// From specifies the source (by alias) and the glob pattern to match files.
	// The format is "@<alias>/<glob-pattern>". When pathPattern is set,
	// the path may use capture placeholders such as "{app}".
	// +kubebuilder:validation:Pattern="^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)/(.*)$"
	// +kubebuilder:validation:MinLength=1
	// +kubebuilder:validation:MaxLength=1024
	// +required
	From string `json:"from"`

	// To specifies the destination path within the artifact.
	// The format is "@artifact/path", the alias "artifact"
	// refers to the root path of the generated artifact. When pathPattern
	// is set, the path may use capture placeholders such as "{app}".
	// +kubebuilder:validation:Pattern="^@artifact/([^/]{0,1}|[^./][^/]|[.][^./]|[^/]{3,})(/([^/]{0,1}|[^./][^/]|[.][^./]|[^/]{3,}))*$"
	// +kubebuilder:validation:MinLength=1
	// +kubebuilder:validation:MaxLength=1024
	// +required
	To string `json:"to"`

	// Exclude specifies a list of glob patterns to exclude
	// files and dirs matched by the 'From' field. Patterns are matched
	// against paths relative to the source alias root or to the non-glob
	// prefix of 'From'. Patterns without a separator (e.g. "*.md") match
	// the file name at any depth.
	// +kubebuilder:validation:MaxItems=100
	// +kubebuilder:validation:items:MaxLength=1024
	// +optional
	Exclude []string `json:"exclude,omitempty"`

	// Strategy specifies the copy strategy to use.
	// 'Overwrite' will overwrite existing files in the destination.
	// 'Merge' is for merging YAML files using Helm values merge strategy.
	// 'Extract' is for extracting the contents of tarball archives (.tar.gz, .tgz)
	// When using glob patterns, non-tarball files are silently skipped. For single file sources,
	// the file must be a tarball or an error is returned. Directories are not supported.
	// If not specified, defaults to 'Overwrite'.
	// +optional
	// +kubebuilder:validation:Enum=Overwrite;Merge;Extract
	Strategy string `json:"strategy,omitempty"`
}

// ArtifactGeneratorStatus defines the observed state of ArtifactGenerator.
type ArtifactGeneratorStatus struct {
	gotkmeta.ReconcileRequestStatus `json:",inline"`

	// Conditions holds the conditions for the ArtifactGenerator.
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`

	// Inventory contains the list of generated ExternalArtifact references.
	// +optional
	Inventory []ExternalArtifactReference `json:"inventory,omitempty"`

	// ObservedSourcesDigest is a hash representing the current state of
	// all the sources referenced by the ArtifactGenerator.
	// +optional
	ObservedSourcesDigest string `json:"observedSourcesDigest,omitempty"`
}

// ExternalArtifactReference contains the reference to a
// generated ExternalArtifact along with its digest.
type ExternalArtifactReference struct {
	// Name of the referent artifact.
	// +required
	Name string `json:"name"`

	// Namespace of the referent artifact.
	// +required
	Namespace string `json:"namespace"`

	// Digest of the referent artifact.
	// +required
	Digest string `json:"digest"`

	// Filename is the name of the artifact file.
	// +required
	Filename string `json:"filename"`
}

// GetConditions returns the status conditions of the object.
func (in *ArtifactGenerator) GetConditions() []metav1.Condition {
	return in.Status.Conditions
}

// SetConditions sets the status conditions on the object.
func (in *ArtifactGenerator) SetConditions(conditions []metav1.Condition) {
	in.Status.Conditions = conditions
}

// GetRequeueAfter returns the duration after which the ArtifactGenerator
// must be reconciled again.
func (in *ArtifactGenerator) GetRequeueAfter() time.Duration {
	return time.Hour
}

// SetLastHandledReconcileAt sets the last handled reconcile time in the status.
func (in *ArtifactGenerator) SetLastHandledReconcileAt(value string) {
	in.Status.LastHandledReconcileAt = value
}

// IsDisabled returns true if the object has the reconcile annotation set to 'disabled'.
func (in *ArtifactGenerator) IsDisabled() bool {
	val, ok := in.GetAnnotations()[ReconcileAnnotation]
	return ok && strings.ToLower(val) == DisabledValue
}

// HasArtifactInInventory returns true if the artifact with the given
// kind, name, namespace, and digest exists in the inventory.
func (in *ArtifactGenerator) HasArtifactInInventory(name, namespace, digest string) bool {
	for _, ref := range in.Status.Inventory {
		if ref.Name == name && ref.Namespace == namespace && ref.Digest == digest {
			return true
		}
	}
	return false
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=ag,categories=all;fluxcd;fluxcd-sources
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
// +kubebuilder:metadata:annotations="kustomize.toolkit.fluxcd.io/substitute=disabled"

// ArtifactGenerator is the Schema for the artifactgenerators API.
type ArtifactGenerator struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   ArtifactGeneratorSpec   `json:"spec,omitempty"`
	Status ArtifactGeneratorStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// ArtifactGeneratorList contains a list of ArtifactGenerator.
type ArtifactGeneratorList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []ArtifactGenerator `json:"items"`
}

func init() {
	SchemeBuilder.Register(&ArtifactGenerator{}, &ArtifactGeneratorList{})
}
